终极版:分布式唯⼀ID⼏种⽣成⽅案(含雪花算法、UUID、
数据库⾃增)
在业务开发中,⼤量场景需要唯⼀ID来进⾏标识:⽤户需要唯⼀⾝份标识、商品需要唯⼀标识、消息需要唯⼀标识、事件需要唯⼀标识等,都需要全局唯⼀ID,尤其是复杂的分布式业务场景中全局唯⼀ID更为重要。
那么,分布式唯⼀ID有哪些特性或要求呢?
① 唯⼀性:⽣成的ID全局唯⼀,在特定范围内冲突概率极⼩。
② 有序性:⽣成的ID按某种规则有序,便于数据库插⼊及排序。
③ 可⽤性:可保证⾼并发下的可⽤性, 确保任何时候都能正确的⽣成ID。
④ ⾃主性:分布式环境下不依赖中⼼认证即可⾃⾏⽣成ID。
⑤ 安全性:不暴露系统和业务的信息, 如:订单数,⽤户数等。
分布式唯⼀ID有哪些⽣成⽅法呢?
总的来说,⼤概有三⼤类⽅法,分别是:数据库⾃增ID、UUID⽣成、snowflake雪花算法。
下⾯分别说下这三⼤类及其优化⽅案:
⼀、数据库⾃增ID
核⼼思想:使⽤数据库的id⾃增策略(如: Mysql的auto_increment)。
优点:
① 简单,天然有序。
缺点:
① 并发性不好。
② 数据库写压⼒⼤。
③ 数据库故障后不可使⽤。
④ 存在数量泄露风险。
针对以上缺点,有以下⼏种优化⽅案:
1. 数据库⽔平拆分,设置不同的初始值和相同的⾃增步长
核⼼思想:将数据库进⾏⽔平拆分,每个数据库设置不同的初始值和相同的⾃增步长。
如图所⽰,可保证每台数据库⽣成的ID是不冲突的,但这种固定步长的⽅式也会带来扩容的问题,很容易想到当扩容时会出现⽆ID初始值可分的窘境,解决⽅案有:
① 根据扩容考虑决定步长。
② 增加其他位标记区分扩容。
这其实都是在需求与⽅案间的权衡,根据需求来选择最适合的⽅式。
2. 批量缓存⾃增ID
java生成随机数的方法核⼼思想:如果使⽤单台机器做ID⽣成,可以避免固定步长带来的扩容问题(⽅案1的缺点)。
具体做法是:每次批量⽣成⼀批ID给不同的机器去慢慢消费,这样数据库的压⼒也会减⼩到N分之⼀,且故障后可坚持⼀段时间。
如图所⽰,但这种做法的缺点是服务器重启、单点故障会造成ID不连续。
还是那句话,没有最好的⽅案,只有最适合的⽅案。
3. Redis⽣成ID
核⼼思想:Redis的所有命令操作都是单线程的,本⾝提供像 incr 和 increby 这样的⾃增原⼦命令,所以能保证⽣成的 ID 肯定是唯⼀有序的。
优点:
① 不依赖于数据库,灵活⽅便,且性能优于数据库。
② 数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点:
① 如果系统中没有Redis,还需要引⼊新的组件,增加系统复杂度。
② 需要编码和配置的⼯作量⽐较⼤。
优化⽅案:
考虑到单节点的性能瓶颈,可以使⽤ Redis 集来获取更⾼的吞吐量,并利⽤上⾯的⽅案(①数据库⽔平拆分,设置不同的初始值和相同的步长; ②批量缓存⾃增ID)来配置集。
PS:⽐较适合使⽤ Redis 来⽣成每天从0开始的流⽔号。⽐如:“订单号=⽇期+当⽇⾃增长号”,则可以每天在Redis中⽣成⼀个Key,使⽤INCR进⾏累加。
⼆、UUID⽣成
核⼼思想:结合机器的⽹卡(基于名字空间/名字的散列值MD5/SHA1)、当地时间(基于时间戳&时钟序列)、⼀个随记数来⽣成UUID。
其结构如下:
aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee(即包含32个16进制数字,以连字号-分为五段,最终形成“8-4-4-4-12”的36个字符的字符串,即32个英数字母+4个连字号)。例如:550e8400-e29b-41d4-a716-446655440000
优点:
① 本地⽣成,没有⽹络消耗,⽣成简单,没有⾼可⽤风险。
缺点:
① 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表⽰,很多场景不适⽤。
② 信息不安全:基于MAC地址⽣成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被⽤于寻梅丽莎病毒的制作者位置。
③ ⽆序查询效率低:由于⽣成的UUID是⽆序不可读的字符串,所以其查询效率低。
◆ 到⽬前为⽌业界⼀共有5种⽅式⽣成UUID:
①版本1 - 基于时间的UUID(date-time & MAC address):
规则:主要依赖当前的时间戳及机器mac地址,因此可以保证全球唯⼀性。
优点:能基本保证全球唯⼀性。
缺点:使⽤了Mac地址,因此会暴露Mac地址和⽣成时间。
②版本2 - 分布式安全的UUID(date-time & group/user id):
规则:将版本1的时间戳前四位换为POSIX的UID或GID,很少使⽤。
优点:能保证全球唯⼀性。
缺点:很少使⽤,常⽤库基本没有实现。
③版本3 - 基于名字空间的UUID-MD5版(MD5 hash & namespace):
规则:基于指定的名字空间/名字⽣成MD5散列值得到,标准不推荐。
优点:不同名字空间或名字下的UUID是唯⼀的;相同名字空间及名字下得到的UUID保持重复。
缺点:MD5碰撞问题,只⽤于向后兼容,后续不再使⽤。
④版本4 - 基于随机数的UUID(pseudo-random number):
规则:基于随机数或伪随机数⽣成。
优点:实现简单。
缺点:重复⼏率可计算。机率也与随机数产⽣器的质量有关。若要避免重复机率提⾼,必须要使⽤基于密码学上的强伪随机数产⽣器来⽣成值才⾏。
⑤版本5 - 基于名字空间的UUID-SHA1版(SHA-1 hash & namespace):
规则:将版本3的散列算法改为SHA1。
优点:不同名字空间或名字下的UUID是唯⼀的;相同名字空间及名字下得到的UUID保持重复。
缺点:SHA1计算相对耗时。
总得来说:
①版本 1/2 适⽤于需要⾼度唯⼀性且⽆需重复的场景。
②版本 3/5 适⽤于⼀定范围内唯⼀且需要或可能会重复⽣成UUID的环境下。
③版本 4 适⽤于对唯⼀性要求不太严格且追求简单的场景。
相关伪代码如下:
// 版本 1 - 基于时间的UUID:
gen_uuid() {
struct uuid uu;
// 获取时间戳
get_time(&clock_mid, &uu.time_low);
uu.time_mid = (uint16_t) clock_mid; // 时间中间位
uu.time_hi_and_version = ((clock_mid >> 16) & 0x0FFF) | 0x1000; // 时间⾼位 & 版本号
/
/ 获取时钟序列。在libuuid中,尝试取时钟序列+1,取不到则随机;在python中直接使⽤随机
get_clock(&uu.clock_seq);// 时钟序列+1 或随机数
uu.clock_seq |= 0x8000;// 时钟序列位 & 变体值
// 节点值
char node_id[6];
get_node_id(node_id);// 根据mac地址等获取节点id
return uu;
}
// 版本4 - 基于随机数的UUID:
gen_uuid() {
struct uuid uu;
uuid_t buf;
random_get_bytes(buf, sizeof(buf));// 获取随机出来的uuid,如libuuid根据进程id、当⽇时间戳等进⾏srand随机
uu.clock_seq = (uu.clock_seq & 0x3FFF) | 0x8000;// 变体值覆盖
uu.time_hi_and_version = (uu.time_hi_and_version & 0x0FFF) | 0x4000;// 版本号覆盖
return uu;
}
// 版本5 - 基于名字空间的UUID(SHA1版):
gen_uuid(name) {
struct uuid uu;
uuid_t buf;
sha_get_bytes(name, buf, sizeof(buf));// 获取name的sha1散列出来的uuid
uu.clock_seq = (uu.clock_seq & 0x3FFF) | 0x8000;// 变体值覆盖
uu.time_hi_and_version = (uu.time_hi_and_version & 0x0FFF) | 0x5000;// 版本号覆盖
return uu;
}
三、雪花算法
核⼼思想:把64-bit分别划分成多段,分开来标⽰机器、时间、某⼀并发序列等,从⽽使每台机器及同⼀机器⽣成的ID都是互不相同。
PS:这种结构是雪花算法提出者Twitter的分法,但实际上这种算法使⽤可以很灵活,根据⾃⾝业务的并发情况、机器分布、使⽤年限等,可以⾃由地重新决定各部分的位数,从⽽增加或减少某部分的量级。⽐如:百度的UidGenerator、美团的Leaf等,都是基于雪花算法做⼀些适合⾃⾝业务的变化。
下⾯介绍雪花算法的⼏种不同优化⽅案:
1. Twitter的snowflake算法
核⼼思想是:采⽤bigint(64bit)作为id⽣成类型,并将所占的64bit 划分成多段。
其结构如下:
说明:
①1位标识:由于long基本类型在Java中是带符号的,最⾼位是符号位,正数是0,负数是1,所以id⼀般是正数,最⾼位是0。
②41位时间截(毫秒级):需要注意的是,41位时间截不是存储当前时间的时间截,⽽是存储时间截的差值(当前时间截 - 开始时间截)得
到的值,这⾥的开始时间截,⼀般是指我们的id⽣成器开始使⽤的时间截,由我们的程序来指定。41位的毫秒时间截,可以使⽤69年(即T
=(1L << 41)/(1000 * 60 * 60 * 24 * 365)= 69)。
③10位的数据机器位:包括5位数据中⼼标识Id(datacenterId)、5位机器标识Id(workerId),最多可以部署1024个节点(即1 << 10 = 1024)。超过这个数量,⽣成的ID就有可能会冲突。
④12位序列:毫秒内的计数,12位的计数顺序号⽀持每个节点每毫秒(同⼀机器,同⼀时间截)产⽣4096个ID序号(即1 << 12 =
4096)。
PS:全部结构标识(1+41+10+12=64)加起来刚好64位,刚好凑成⼀个Long型。
优点:
①整体上按照时间按时间趋势递增,后续插⼊索引树的时候性能较好。
②整个分布式系统内不会产⽣ID碰撞(由数据中⼼标识ID、机器标识ID作区分)
③本地⽣成,且不依赖数据库(或第三⽅组件),没有⽹络消耗,所以效率⾼(每秒能够产⽣26万ID左右)。
缺点:
①由于雪花算法是强依赖于时间的,在分布式环境下,如果发⽣时钟回拨,很可能会引起ID重复、ID乱序、服务会处于不可⽤状态等问题。
解决⽅案有:
a. 将ID⽣成交给少量服务器,并关闭时钟同步。
b. 直接报错,交给上层业务处理。
c. 如果回拨时间较短,在耗时要求内,⽐如5ms,那么等待回拨时长后再进⾏⽣成。
d. 如果回拨时间很长,那么⽆法等待,可以匀出少量位(1~2位)作为回拨位,⼀旦时钟回拨,将回拨位加1,可得到不⼀样的ID,2位回
拨位允许标记3次时钟回拨,基本够使⽤。如果超出了,可以再选择抛出异常。
Twitter_SnowFlake的源代码(JAVA版):
核⼼运算逻辑(右移运算&位运算): (timestamp << 22) | (datacenterId << 17) | (workerId << 12) | sequence;
/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分⽤-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最⾼位是符号位,正数是0,负数是1,所以id⼀般是正数,最⾼位是0<br>
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,⽽是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这⾥的的开始时间截,⼀般是我们的id⽣成器开始使⽤的时间,由我们程序来指定的(如下下⾯程序IdWorker类的startTime属性)。41位的时间截,可 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号⽀持每个节点每毫秒(同⼀机器,同⼀时间截)产⽣4096个ID序号<br>
* 加起来刚好64位,为⼀个Long型。<br>
* SnowFlake的优点是,整体上按照时间⾃增排序,并且整个分布式系统内不会产⽣ID碰撞(由数据中⼼ID和机器ID作区分),并且效率较⾼,经测试,SnowFlake每秒 */
public class TwitterUidGeneratorUtil {

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。