本文字数:2604字
预计阅读时间:15分钟
01
引言
在现有分布式系统中,面对增长迅速的业务数据,id生成一直是非常重要的一环。而分布式系统的id生成方案需要满足几个重要特性:容错高可用、高性能高并发、全局唯一。
02
技术背景
经过测试,该方案每次生成分布式id的平均耗时为1.46毫秒,而跨机房获取id的平均耗时为5.32毫秒。
这种读数据库方案的弊端就很明显:
当qps过高时,数据库的压力就会大大增加;
跨机房读的耗时也会明显升高;
如果使用框架自带的逻辑,如Hibernate的strategy = GenerationType.TABLE策略,要求sequence表和数据表同数据源,所以当进行分库后,新库对id的获取就会很麻烦;
有很小的概率会在生成id读写数据库时,导致死锁,新增数据无法入库,我们就遇到过一次 - -!!!
当然这种方案有一个最大的好处:产生的id严格递增,对我们业务来说,这个特性非常重要。
03
现有分布式方案分析
分布式id生成这么重要,市面上当然有很多的解决方案。下边简单介绍一下几种常见的方案:
UUID:
介绍:UUID是一个由32个十六进制数字组成,中间由横杠分割(例:372f3ba5-6359-4f1f-9184-a938a4908072),java中可以直接调用UUID.randomUUID()实现,也可以使用特定算法生成。
优点:实现简单,UUID的生成非常简单,不需要依赖于任何外部资源;实际应用中基本不会遇到重复的情况。
缺点:UUID长度较长,占用的存储空间较大,且可读性差;算法实现复杂,经测试存在效率问题。在数据库作为主键时,可能会影响写入性能;不是递增的。
雪花算法:
介绍:SnowFlake是 Twitter 开源的分布式 id 生成算法,可以不用依赖任何第三方工具进行自增的数字类型的id生成;雪花算法的核心逻辑是使用一个 64 bit 的 long 型的数字作为全局唯一id。
雪花算法生成的唯一ID均为正数,所以这 64 个 bit 中,其中 1 个 bit 是不用的(第一个 bit 默认都是 0),然后用 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。
优点:实现简单,不依赖其他第三方库;高效,雪花算法能以极高的速度生成ID,每秒可生成数百万个,满足高并发场景的需求。
缺点:时间回拨问题;生成的id长度比较长;机器码不同,生成的id也不同,跟历史id相比变化比较大;生成的id趋势递增。
百度uid-generator框架:
介绍:百度UidGenerator是基于snowflake算法思想实现的,但与原始算法不同的地方在于,UidGenerator支持自定义时间戳、工作机器id(workId)以及序列号等各个组成部分的位数,并且工作机器id采用用户自定义的生成策略。百度uid-generator有两种实现方式:DefaultUidGenerator和CachedUidGenerator。从性能和时间回拨问题考虑,一般都是考虑CachedUidGenerator类实现。
优点:性能好,每秒可生成数百万个id;简单易用,现成jar包直接接入,包括获取当前时间戳、数据中心id和机器id。支持多种部署方式,包括单机模式和分布式模式。
缺点:默认接入mybatis框架,否则需要自己重写dao层;生成的id趋势递增。
美团leaf框架:
介绍:美团的leaf框架有两种模式。一种是雪花算法模式,也是基于雪花算法这里不再赘述。另一种模式是号段模式。号段模式:每次从数据库中取一个号段的id值,号段由step步长决定。然后把号段放在内存中。当号码使用到一定范围时,则更新到下一号段。
优点:每次取一个号段的id,大大减少了对数据库的读写,减轻了数据库压力;不同的机器存在不同的号段,放在内存中,速度快效率高,只需要考虑本机的线程安全问题。
缺点:如果有多台机器提供服务,那么每台机器生成的号段不同,只能保证趋势递增;如果有一台服务器,对于业务请求量巨大时,单台服务器可能会扛不住压力,服务器宕机就会使获取id服务不可用。
当然除了上述方案,还有其他的分布式id生成方案:比如zookeeper的顺序节点,滴滴的Tinyid框架,这里就不一一列举。
04
严格递增的分布式id生成方案
方案介绍:
采用的是 数据库号段模式 加 缓存 加 监听 的方案,有两种id生成模式:
使用缓存生成;
使用数据库表生成。
具体工具可以自行选择。这里使用的是mysql + redis + nacos。
mysql中创建一张sequence表,主要字段:bizId: 业务表示,区分不同业务的id;maxId:目前号段最大的Id值;step:步长,每个号段包含的id个数。
redis中也需要存储三个key:currentId:当前已经使用到的id值;maxId:当前号段的最大id值,step:步长。
nacos开关的作用:控制是否id生成模式,打开:使用redis生成模式,关闭:使用数据库表生成模式;同时监听nacos开关,控制生成模式自动切换。
实现流程:
项目启动时:如果nacos开关打开,检测redis中是否存在当前业务id相关的key,如果没有则读取数据库加载到redis中(注意先更新到下一号段)
当有新的写入请求时:
首先判断 redis心跳检测正常 且 nacos开关打开,则是缓存生成模式:
直接读取redis中对应currentId,通过incr()方法获取到下一个id值;
此时检查id时否合理,合理阈值可以自行设置。如果不合理则调用 数据库同步redis流程(先把数据库更新到下一号段,然后再更新redis中的值:currentId = 原maxId + step*0.1, 新maxId = 原maxId + step);
生成id后,异步线程判断:当前id是否已经使用了当前号段的百分之30,如果超过则更新获取下一号段。
否则:使用数据库生成模式,直接读取数据库sequence表把maxId字段作为当前id使用,maxId字段先加锁查询,然后再更新加1。
当有特殊情况时:
redis集群不可用,通过心跳任务检测出状态不对,则直接调用api关闭开关。关闭后就是使用数据库模式生成id,虽然耗时有所增加,但增加量不多且可以保证业务流程不阻塞;
处理好redis问题后,修改naocs开关,程序监听到开关打开事件,则从数据库模式改为缓存模式生成:先更新下一号段避免id重复,然后把新更新的值写入redis中,下次请求就继续开始使用redis生成id。
总结:
这个方案使用redis来生成Id,主要是因为redis作为强大的中间件基本所有项目都会用到,随处可见,不用再引入新的第三方依赖;其次redis的自增自带原子性,生成的id是严格递增。
并且redis可以很好的抗住高QPS请求,经测试id的获取绝大部分小于等于1毫秒。
为了防止redis集群抽疯不可用,准备了数据库生成方案:直接读取数据库中的sequence表,查询并更新maxId字段加1。这样可以保证业务的正常运行,耗时平均涨几毫秒,属于可接受范围。在使用数据模式生成期间,就可以着手处理redis集群的问题,处理完后通过监听开关打开事件,再重新切换到缓存生成模式,继续生成严格递增的分布式id。(为了防止重复,先更新数据库到下一号段,把新值更新到redis中)
附流程图:
05
结论
这个方案主要是通过redis的自增来高效生成严格递增的id,可以用其他中间件代替。这个方案重要的是不只依赖于redis,还要对redis不可用的情况进行兜底检测,形成一个自动切换的闭环。
经测试该方案性能,相比于之前直接查询更新数据库sequence表方案,同机房获取id性能提升接近10倍,跨机房获取id性能提升接近7倍。
同时该方案也解决了之前遇到过的数据库sequence表死锁,导致业务数据无法新增入库的问题。
当然如果业务需求并不要求id严格递增,那么上边介绍的优秀的框架都可以使用。