对于分布式系统,生成 唯一ID的方法,大致分为3类:
(1)UUID
(2)依赖数据库的 flicker 方案
(3)twitter 的 snowflake 算法
后面要介绍一种 阿里的 TDDL 中的方案,同样依赖数据库,但是比 Flicker 性能更高
此外,很多公司实际上是采用分布式ID生成系统来解决这种问题的,放弃了对网络IO方面的考虑,做到统一管理,这种方式有利有弊,美团,达达都是采用这种方式,这个问题要怎么看呢,如果服务宕掉,问题还是挺大的。
UUID
UUID是通用唯一识别码 (Universally Unique Identifier),在其他语言中也叫GUID,可以生成一个长度32位的全局唯一识别码。
String uuid = UUID.randomUUID().toString()
结果示例:
046b6c7f-0b8a-43b9-b35d-6489e6daee91
缺点:
- 对于无序的 UUID,如果需要插入 mysql 的话,一般情况下 UUID 是作为主键 的。由于 MySQL 的存储是采用 B+ 树实现的,所以最好要自增,否则对于树扩展的性能会有影响,而且也会造成节点数据不均匀。从存储性能的角度来看的话,这种方法存在一定的弊端
- UUID 16个字节,128位长,通常以 36个字节的字符串表示,能否缩短这个长度?
Flicker方案
- 使用 MySQL 自身的特性实现自增,如果想要提高性能,可以用多个库/表,设置步长,第一个表1,4,7,10…,第二个表2,5,8,11…,第三个表3,6,9,12… 这种方式能够加快 id 生成。
- 使用 Redis ,同样也可以分布到多个 key 上。
缺点:
- 这种方式依赖于数据库
- 每次获取唯一 ID 都需要 IO 操作
SnowFlake方案
SnowFlake所生成的ID一共分成四部分:
1.第一位
占用1bit,其值始终是0,没有实际作用。
2.时间戳
占用41bit,精确到毫秒,总共可以容纳约140年的时间。
3.工作机器id
占用10bit,其中高位5bit是数据中心ID(datacenterId),低位5bit是工作节点ID(workerId),做多可以容纳1024个节点。
4.序列号
占用12bit,这个值在同一毫秒同一节点上从0开始不断累加,最多可以累加到4095。
第一位一般用做符号位
可以看到这是 64位,比 UUID 要缩短一半。而如果用16进制表示,也只用了16个字节,要比36个字节短很多
SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢?只需要做一个简单的乘法:
同一毫秒的ID数量 = 1024 X 4096 = 4194304
这个数字在绝大多数并发场景下都是够用的。
有几点需要解释一下:
1.获得单一机器的下一个序列号,使用Synchronized控制并发,而非CAS的方式,是因为CAS不适合并发量非常高的场景。对于高并发的场景,CAS 会经常失败,所以会增加读的次数。
2.如果当前毫秒在一台机器的序列号已经增长到最大值4095,则使用while循环等待直到下一毫秒。
3.如果当前时间小于记录的上一个毫秒值,则说明这台机器的时间回拨了,抛出异常。但如果这台机器的系统时间在启动之前回拨过,那么有可能出现ID重复的危险。可以看到对于重启来说不是很友好。
在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不是全局递增的情况。
TDDL 中的策略
(1)使用数据库同步ID信息
(2)每次批量去一定数量的可用ID在内存中,使用完后,再请求数据库重新获取下一批可用ID,每次获取量由步长控制
相比于Flicker方案,大大降低数据库写压力,数据库不再是性能瓶颈。
但是种种方式同样是强依赖数据库,当数据库异常时,系统仍然不可用。
这篇博文给出了一种基于 SnowFlake的发号器方案
这是美团的方案