常用的分布式ID方案
UUID
编码规则
UUID全局唯一标识符,由32位的16进制数字组成,所以理论上UUID的总数为16^32 = 2^128个
UUID的标准格式:
16进制数A表示UUID版本,当前规范共有5个版本,所以A的值可以是1/2/3/4/5;
16进制数B表示UUID的变体,B的前二进制位固定为10xx,所以B的取值只能是8/9/a/b;
不同的版本采用不同算法,利用不同的信息源来产生UUID:
- version1, date-time & MAC address
- version2, date-time & group/user id
- version3, MD5 hash & namespace
- version4, pseudo-random number
- version5, SHA-1 hash & namespace
目前使用较多的事version1和version4,version1使用当前时间戳和MAC地址信息。version4使用强(伪)随机数信息。
因为时间戳和强随机数的唯一性,所以version1和version4总是生成唯一的标识符。如果需要针对给定的字符串总是生成相同的UUID,使用version3或version5。
JDK实现
Jdk在java.util.UUID中,针对version4和version3做了相关实现
/**
* Static factory to retrieve a type 4 (pseudo randomly generated) UUID.
*
* The {@code UUID} is generated using a cryptographically strong pseudo
* random number generator.
*
* @return A randomly generated {@code UUID}
*/
public static UUID randomUUID() {
SecureRandom ng = Holder.numberGenerator;
byte[] randomBytes = new byte[16];
ng.nextBytes(randomBytes);
randomBytes[6] &= 0x0f; /* clear version */
randomBytes[6] |= 0x40; /* set to version 4 */
randomBytes[8] &= 0x3f; /* clear variant */
randomBytes[8] |= 0x80; /* set to IETF variant */
return new UUID(randomBytes);
}
/**
* Static factory to retrieve a type 3 (name based) {@code UUID} based on
* the specified byte array.
*
* @param name
* A byte array to be used to construct a {@code UUID}
*
* @return A {@code UUID} generated from the specified array
*/
public static UUID nameUUIDFromBytes(byte[] name) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
throw new InternalError("MD5 not supported", nsae);
}
byte[] md5Bytes = md.digest(name);
md5Bytes[6] &= 0x0f; /* clear version */
md5Bytes[6] |= 0x30; /* set to version 3 */
md5Bytes[8] &= 0x3f; /* clear variant */
md5Bytes[8] |= 0x80; /* set to IETF variant */
return new UUID(md5Bytes);
}
UUID的方案能够适用大部分场景,但是存在几个方面不足:
- UUID字符串占用空间较大,在大数据量时会对存储产生较大影响
- UUID完全随机索引效率比较低
- UUID随机生成做不了递增,在Mysql主键索引的情况下影响插入效率(引起B+树叶子节点分裂和重新平衡)
数据库自增ID
数据库自增ID方案依赖于数据库的自增主键去生成分布式ID,需要先单独建一张表:
CREATE TABLE SEQUENCE_ID (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(10) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=MyISAM;
用如下的语句去获取自增ID:
begin;
replace into SEQUENCE_ID (stub) values ('固定字符串');
select last_insert_id();
commit;
这边的stub字段仅仅用来唯一性约束数据,保证replace语句执行时候能够删除表中历史数据,这样表中的数据只会有一条。
数据库自增ID的方案虽然可行,但是在高可用和性能上面都存在不足。
高可用,如果当前数据库实例下线或崩溃,那分布式ID就不可用了会影响正常业务;
性能,每获取一次分布式ID都需要访问一次数据库,高并发下性能存在问题;
优化方案(数据库多主模式):
高可用通常采用冗余的方案,同时使用多个数据库实例,其中一个或多个实例下线或崩溃只要有数据库实例正常就能保证分布式ID服务可用。这种方案需要我们对数据库自增主键进行起始值和自增步长进行设置。
第一台Mysql实例配置:
set @@auto_increment_offset = 1; --起始值
set @@auto_increment_increment = n; --步长 这边指的是Mysql实例总数
第二台Mysql实例配置:
set @@auto_increment_offset = 2; --起始值
set @@auto_increment_increment = n; --步长 这边指的是Mysql实例总数
......
第n台Mysql实例配置:
set @@auto_increment_offset = n; --起始值
set @@auto_increment_increment = n; --步长 这边指的是Mysql实例总数
对于这种生成分布式ID的方案,还需要单独新增一个生成分布式ID的分布式应用,比如UidGenerateService,该应用提供一个接口供业务方获取ID,业务方需要ID时,通过RPC的方式请求UidGenerateService,UidGenerateService
多数据库实例能在一定程度上缓解性能压力,但是不能从根本上解决问题。
数据库多主模式的方案还存在一个问题,数据库实例在扩展的时候需要停机修改所有实例的步长值。
号段模式
为了解决“数据库自增ID”和“数据库多主模式”每获取一次分布式ID都要请求数据库而导致的性能问题,引入了号段模式的方案。号段可以理解成批量获取,比如UidGenerateService每次从数据库获取ID时,获取一个号段比如[1,1000]用这个范围来表示1000个ID,UidGenerateService只需要在本地从1开始自增并返回ID,等到当前号段ID被用完,再去数据区请求下一个号段。
号段模式的数据库表如下:
id | biz_type | max_id | step | version |
---|---|---|---|---|
1 | 1000 | 2000 | 1000 | 0 |
- biz_type表示业务类型,用来隔离不同的业务类型
- max_id表示的最大的可用id
- step代表号段的长度,可以根据不同业务的qps去设置合理的长度
- version是乐观锁,每次更新都要加上version,保证并发更新的正确性
获取可用号段时,首先查询当前业务的号段信息,计算新的max_id然后更新数据库中的max_id,更新成功就认为号段获取成功新的号段为(旧max_id,新max_id],更新失败说明号段被其他线程获取需要进行重试。
号段模式的方案存在以下的问题:
- 使用了分布式的ID生成应用无法保证单调递增
- 数据库层面还是单实例可用性无法保证
雪花算法
snowflake是twitter开源的分布式ID生成算法和“数据库自增ID”、“号段模式”的方案不一样,不依赖于数据库,snowflake的方案是。
snowflake的分布式ID固定是一个long型的数值,也就是64bit位,原始snowflake算法中对于bit分配如下:
- 第一个bit位不使用,java中的long最高位是符号位,ID一般为正数,所以固定为0
- 时间戳的41bit位记录的是毫秒时间,一般存储时间戳的差值(当前时间-固定的开始时间),这样产生的ID会从更小值开始,41bit的时间戳可以 使用69年
- 工作机器ID占用10bit,可以标志1024个节点,10bit可以分开标识机房以及机器
- 序列号占用12bit,支持每毫秒每个节点生成4096个ID
snowflake存在以下问题:
- 时钟回拨问题
- 不能在一台机器上部署多个分布式ID服务
- 12bit的序列号生成方案如果固定起始值递增,在QPS低的且存在分库分表的场景时会导致数据倾斜(生成的ID低位一直为0,来自shardingsphere的bug实例)
Redis
Reids生成分布式ID的方案和“数据库自增ID”的方案比较类似,使用redis的原子性自增命令来实现ID生成:
127.0.0.1:6379> set SEQ_ID 1 //设置自增ID字段
OK
127.0.0.1:6379> incr SEQ_ID //增加1,并返回
(integer) 2
使用Redis的优势在于性能比较高,但是需要考虑持久化的问题,目前Redis支持RDB和AOF两种持久化方式。
- RDB持久化是类似快照的方式进行全量持久化,同步时如果有ID生成没能序列化下来,节点宕机重启后会出现重复ID
- AOF持久化是针对写命令追加到文件中进行增量持久化,根据配置策略在节点宕机重启后可以不丢失数据
Redis方案存在以下问题:
- Redis在高可用方面存在不足,单节点宕机后导致ID生成服务不可用
优化方案:
采用类似数据库多主的方案,用多个独立的Redis实例改用incrby命令和指定步长+ID生成分布式服务实现高可用
节点1
127.0.0.1:6379> set SEQ_ID 1 //设置自增ID字段
OK
127.0.0.1:6379> incrby SEQ_ID 2 //增加2,并返回
(integer) 3
节点2
127.0.0.1:6379> set SEQ_ID 2 //设置自增ID字段
OK
127.0.0.1:6379> incrby SEQ_ID 2 //增加2,并返回
(integer) 4
优化方案的缺点和数据库多主模式一致,存在不能灵活扩展的问题(需要同步调整起始值和步长)
美团Leaf
美团的Leaf框架,相对比较全面,同时支持号段模式和snowflake模式
https://github.com/Meituan-Dianping/Leaf
号段模式
采用的是上述的标准的号段模式,在此基础上采用双buffer模式
snowflake模式
- leaf的雪花模式弱依赖zookeeper,使用zk的初始顺序节点来进行workerId配置
- 时间回拨问题直接失败并报警
滴滴Tinyid
https://github.com/didi/tinyid
- 在leaf的号段模式基础上做的增强
- 数据库层面使用多主模式保证数据库层面高可用
- tinyid提供了客户端的方式使得可以在业务方缓存号段ID
百度UidGenerator
https://github.com/baidu/uid-generator
百度的ID生成器是雪花算法的变种,分为两种DefaultUidGenerator和CachedUidGenerator
sign | delta seconds | worker node id | sequence |
---|---|---|---|
1bit | 28bits | 22bits | 13bits |
时间部分使用了28bit位采用秒计数,只能承受8.5年,可以根据业务需求调整占用位数。
DefaultUidGenerator
- worker id是用的DefaultUidGenerator初始化插入数据库后主键ID
- 如果发生时间回拨直接抛出异常
- 新的一秒的话sequence从0开始
CachedUidGenerator
是对DefaultUidGenerator的增强
- 采用双RingBuffer来缓存生成的ID
- 采用时间递增的方式获取时间而不是获取系统时间