分布式架构系统生成全局唯一序列号(研究)

分布式架构系统生成全局唯一序列号(研究)

【背景】: 分布式架构下,唯一序列号生成是我们在设计一个系统,尤其是数据库使用分库分表的时候常常会遇见的问题。当分成若干个sharding表后,如何能够快速拿到一个唯一序列号,是经常遇到的问题。

【特性需求】: 

1. 全局唯一;2. 支持高并发;3. 能够体现一定属性;4. 高可靠,容错单点故障;5. 高性能

【业内解决方案】:

(1)利用数据库递增,全数据库唯一。优点:明显,可控。缺点:单库单表,数据库压力大

(2)UUID, 生成的是length=32的16进制格式的字符串,如果回退为byte数组共16个byte元素,即UUID是一个128bit长的数字,一般用16进制表示。优点:对数据库压力减轻了。缺点:但是排序怎么办?此外还有UUID的变种,增加一个时间拼接,但是会造成id非常长。

(3)twitter在把存储系统从MySQL迁移到Cassandra的过程中由于Cassandra没有顺序ID生成机制,于是自己开发了一套全局唯一ID生成服务:Snowflake。优点:高性能,低延迟;独立的应用;按时间有序。缺点:需要独立的开发和部署

1). 41位的时间序列(精确到毫秒,41位的长度可以使用69年)
2). 10位的机器标识(10位的长度最多支持部署1024个节点) 
3). 12位的计数顺序号(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) 最高位是符号位,始终为0

(4)Redis生成ID : 当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作INCR和INCRBY来实现。

举例:

可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:

A:1,6,11,16,21 ;   B:2,7,12,17,22 ;  C:3,8,13,18,23;  D:4,9,14,19,24;  E:5,10,15,20,25

比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

 优点:

不依赖于数据库,灵活方便,且性能优于数据库。数字ID天然排序,对分页或者需要排序的结果很有帮助。使用Redis集群也可以防止单点故障的问题。

缺点:

如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。需要编码和配置的工作量比较大,多环境运维很麻烦,在开始时,程序实例负载到哪个redis实例一旦确定好,未来很难做修改。

(5) Flicker的解决方案

因为MySQL本身支持auto_increment操作,很自然地,我们会想到借助这个特性来实现这个功能。Flicker在解决全局ID生成方案里就采用了MySQL自增长ID的机制(auto_increment + replace into + MyISAM)

(6)其他方案

比如京东淘宝等电商的订单号生成。因为订单号和用户id在业务上的区别,订单号尽可能要多些冗余的业务信息,比如:

滴滴:时间+起点编号+车牌号; 淘宝订单:时间戳+用户ID;其他电商:时间戳+下单渠道+用户ID,有的会加上订单第一个商品的ID。而用户ID,则要求含义简单明了,包含注册渠道即可,尽量短。

(7)最终方案

 以flicker方案为基础进行优化改进。具体实现是,单表递增,内存缓存号段的方式:

首先建一张表:SEQUENCE_GENERATOR_TABLE(其中id是自增的,stub是服务器ip)

id     stub

1      192.168.1.1

使用mysql的独有语法 replace to来更新记录来获得唯一id: 

REPLACE INTO SEQUENCE_GENERATOR_TABLE (stub) VALUES ("192.168.1.1");
SELECT id FROM SEQUENCE_GENERATOR_TABLEWHERE stub = "192.168.1.1";   把它拿回来

到上面为止,我们只是在单台数据库上生成ID,从高可用角度考虑,接下来就要解决单点故障问题 :

为了防止多服务器同时更新数据,取回的id混淆的问题。设置机器ip字段,当多个服务器的时候,这个表是这样的:

id             stub

5       192.168.1.1

2       192.168.1.2

3       192.168.1.3

4       192.168.1.4

-----> 每台服务器只更新自己的那条记录,保证了单线程操作单行记录。这时候每个机器拿到的分别是5,2,3,4这4个id。至此,我们似乎解决这个服务器隔离,原子性获得id的问题,也和flicker方案基本一致。

【原理】: 

以上方案还是依靠数据库的特性,每次生成id都要请求db,开销很大。

【进行优化】:把这个id作为一个号段,而并不是要发出去的序列号,并且这个号段是可以配置长度的,可以1000也可以10000,也就是对拿回来的这个id放大多少倍的问题。

我们从DB一次查询操作的开销,拿回来了1000个用户id到内存中了 ,现在的问题就是要解决同一台服务器在高并发场景,让大家顺序拿号,别拿重复,也别漏拿(保持这个号段对象隔离性的问题)。

【解决方案】: 

采用AtomicLong,区间内的第一个号码 :atomic.set(n * 1000)【首】,当第一次拿回号段id后,扩大1000倍,然后赋值给这个变量atomic;

并且内存里保存一下最大id,也就是这个号段的最后一个号码:currentMaxId = (n + 1) * 1000【末】

【atomic.set(n * 1000),currentMaxId = (n + 1) * 1000】

如果到达了最后一个号码,那么阻塞住其他请求线程,最早的那个线程去db取个号段,再更新一下号段的两个值,就可以了。这个方案,核心代码逻辑不到20行,解决了分布式系统序列号生成的问题。

【小问题】就是在服务器重启后,因为号码缓存在内存,会浪费掉一部分用户ID没有发出去,所以在可能频繁发布的应用中,尽量减小号段放大的步长n,能够减少浪费。 

经过实践,性能的提升远远重要于浪费一部分id。如果再追求极致,可以监听spring或者servlet上下文的销毁事件,把当前即将发出去的用户ID保存起来,下次启动时候再捞回内存即可。 

【流程图】:

【参考文献】: 干货 | 分布式架构系统生成全局唯一序列号的一个思路​​​​​​​

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值