如何生成严格递增的分布式id?


本文字数:2604

预计阅读时间:15分钟

01

引言

在现有分布式系统中,面对增长迅速的业务数据,id生成一直是非常重要的一环。而分布式系统的id生成方案需要满足几个重要特性:容错高可用、高性能高并发、全局唯一。

02

技术背景

我们系统中最开始使用的是通过数据库表生成对应的分布式id,数据库中存一张sequence表,只有一行数据,记录下一个id值。每次有新数据生成,都开启事务加锁查询当前值,然后再更新字段值加1。

经过测试,该方案每次生成分布式id的平均耗时为1.46毫秒,而跨机房获取id的平均耗时为5.32毫秒。

这种读数据库方案的弊端就很明显:

  1. 当qps过高时,数据库的压力就会大大增加;

  2. 跨机房读的耗时也会明显升高;

  3. 如果使用框架自带的逻辑,如Hibernate的strategy = GenerationType.TABLE策略,要求sequence表和数据表同数据源,所以当进行分库后,新库对id的获取就会很麻烦;

  4. 有很小的概率会在生成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生成方案

上述中的那些方案,可以看到除了使用zk的顺序节点,其他都是只能保证趋势递增,并不能保证严格递增(后请求的数据id,一定比先请求的数据id大)。对于我们业务来说,严格递增id非常有必要,而使用zk又需要维护一套高可用的zk集群。所以学习前人们的解决方案之后,诞生了我们自己的分布式id生成方案。

方案介绍

采用的是 数据库号段模式 加 缓存 加 监听 的方案,有两种id生成模式:

  1. 使用缓存生成;

  2. 使用数据库表生成。

具体工具可以自行选择。这里使用的是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中(注意先更新到下一号段)

当有新的写入请求时:

  1. 首先判断 redis心跳检测正常 且 nacos开关打开,则是缓存生成模式:

    1. 直接读取redis中对应currentId,通过incr()方法获取到下一个id值;

    2. 此时检查id时否合理,合理阈值可以自行设置。如果不合理则调用 数据库同步redis流程(先把数据库更新到下一号段,然后再更新redis中的值:currentId = 原maxId + step*0.1, 新maxId = 原maxId + step);

    3. 生成id后,异步线程判断:当前id是否已经使用了当前号段的百分之30,如果超过则更新获取下一号段。

  2. 否则:使用数据库生成模式,直接读取数据库sequence表把maxId字段作为当前id使用,maxId字段先加锁查询,然后再更新加1。

当有特殊情况时:

  1. redis集群不可用,通过心跳任务检测出状态不对,则直接调用api关闭开关。关闭后就是使用数据库模式生成id,虽然耗时有所增加,但增加量不多且可以保证业务流程不阻塞;

  2. 处理好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严格递增,那么上边介绍的优秀的框架都可以使用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值