分布式全局唯一ID生成方案

2019-10-06  本文已影响0人  小星的java学习笔记

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。比如京东等电商平台的订单ID,微信等IM应用的消息ID等。业务系统对ID号的要求大致有下面这些(不能全部满足,有些要求是互斥的):

    1.全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求

    2.趋势递增:在一段时间内,生成的ID是递增的趋势。如:再一段时间内生成的ID在【0,1000】之间,过段时间生成的ID在【1000,2000】之间。但在【0-1000】区间内的时候,ID生成有可能第一次是12,第二次是10,第三次是14。

    3.单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。

    4.ID要尽量简短,保证较高的查询效率。

    5.信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞争对手可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。

同时,由于业务系统强依赖于ID号,如果ID生成系统瘫痪,这就会带来一场灾难,所以对ID系统的性能和稳定性要求如下:

1.平均延迟和TP999延迟都要尽可能低;2.可用性5个9;3.高QPS;4.高稳定性,高可用。

方案一:UUID

UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000。

优点:

代码实现简单。

本机生成,没有性能问题

因为是全球唯一的ID,所以迁移数据容易

缺点:

每次生成的ID是无序的,无法保证趋势递增

查询效率慢,如果作为数据库主键,在InnoDB引擎下,使用的是B+tree索引,如果我们的插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。

存储空间大,UUID太长,16字节128位,通常以36长度的字符串表示

ID本事无业务含义,不可读

信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

方案二:雪花snowflake算法

雪花算法生成64位的二进制正整数,然后转换成10进制的数。64位二进制数由如下部分组成:


1位标识符:始终是0;41位时间戳:41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截 )得到的值,这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的;10位机器标识码:可以部署在1024个节点,如果机器分机房(IDC)部署,这10位可以由 5位机房ID + 5位机器ID 组成;12位序列:毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。

优点:

此方案每秒能够产生409.6万个ID,性能快

时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增

灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求

缺点:

依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成

在分布式场景中,服务器时钟回拨会经常遇到,一般存在10ms之间的回拨;小伙伴们就说这点10ms,很短可以不考虑吧。但此算法就是建立在毫秒级别的生成方案,一旦回拨,就很有可能存在重复ID。那么如何解决时钟回拨问题呢?

参见上图整个启动流程图,服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点:

若写过,则用自身系统时间与leaf_forever/${self}节点记录时间做比较,若小于leaf_forever/${self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警。

若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。

若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约。

否则认为本机系统时间发生大步长偏移,启动失败并报警。

每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}。

由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警。

方案三:Redis生成ID

这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

优点:

不依赖于数据库,灵活方便,且性能优于数据库。

数字ID天然排序并自增,对分页或者需要排序的结果很有帮助。

缺点:

占用带宽,每次要向redis进行请求。

如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。

需要编码和配置的工作量比较大。

强依赖redis,redis出问题业务不可用。

方案四:利用zookeeper生成唯一ID

zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。很少会使用zookeeper来生成唯一ID。主要是由于需要依赖zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此性能在高并发的分布式环境下不甚理想。

方案五:数据库生成

以MySQL举例,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。

优点:

非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。

ID号趋势递增(不分库的话就是单调递增),可以实现一些对ID有特殊要求的业务。

缺点:

强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。

一旦把步长定好后,就无法扩容;而且单个数据库的压力大,数据库自身性能无法满足高并发。

方案六:数据库segment方案

原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。 - 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。

1、【用户服务】在注册一个用户时,需要一个用户ID;会请求【生成ID服务(是独立的应用)】的接口

2、【生成ID服务】会去查询数据库,找到user_tag的id,现在的max_id为0,step=1000

3、【生成ID服务】把max_id和step返回给【用户服务】;并且把max_id更新为max_id = max_id + step,即更新为1000

4、【用户服务】获得max_id=0,step=1000;

5、 这个用户服务可以用ID=【max_id + 1,max_id+step】区间的ID,即为【1,1000】

6、【用户服务】会把这个区间保存到jvm中

7、【用户服务】需要用到ID的时候,在区间【1,1000】中依次获取id,可采用AtomicLong中的getAndIncrement方法。

8、如果把区间的值用完了,再去请求【生产ID服务】接口,获取到max_id为1000,即可以用【max_id + 1,max_id+step】区间的ID,即为【1001,2000】

优点:

生成ID服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。

ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。

容灾性高:在一段区间内,是在jvm内存中获取的。即使数据库宕机了,系统也不受影响,ID还能维持一段时间。

可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来。

缺点:

ID号码不够随机,能够泄露发号数量的信息,不太安全。

TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。

DB宕机会造成整个系统不可用。

以上方案中,如果是多个用户服务,同时获取ID,同时去请求【ID服务】,在获取max_id的时候会存在并发问题。可以加分布式锁,保证同一时刻只有一个用户服务获取max_id。当然也可以用数据库自身的锁去解决。

利用事务方式加行锁,上面的语句,在没有执行完之前,是不允许第二个用户服务请求过来的,第二个请求只能阻塞。可以采用双buffer方案来解决阻塞问题:

因为会有一个线程,会观察什么时候去自动获取。两个buffer之间自行切换使用。就解决了突发阻塞的问题。

总结:

业务系统根据自身目前架构和业务要求选择适合自己的ID生成方案。


参考文章:

/Leaf——美团点评分布式ID生成系统

一线大厂的分布式唯一ID生成方案是什么样的?

分布式系统全局唯一ID简介、特点、生成

漫画:什么是SnowFlake算法?

分布式ID生成器 | 架构师之路

上一篇下一篇

猜你喜欢

热点阅读