在分布式系统中,经常需要对大量的数据、消息、http请求等进行唯一标识,例如:在分布式系统之间http请求需要唯一标识,调用链路分析的时候需要使用这个唯一标识。这个时候数据库自增主键已经不能满足需求,需要一个能够生成全局唯一ID的系统,这个系统需要满足以下需求:
UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符。
优点:
性能非常高:本地生成,没有网络消耗。
缺点:
1、不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
2、信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
3、ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求。
据我了解snowflake方案是采用的比较多的一种分布式id生成方案。这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等。41-bit的时间可以表示(1L<<41)/(1000L*3600*24*365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。
优点:
1.毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
2. 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
3. 可以根据自身业务特性分配bit位,非常灵活。
缺点:
以MySQL举例,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。
项目地址:https://github.com/FantasyPig/leaves
调查了三种分布式ID方案后,我决定自己实现一个ID生成器,名为leaves。关于这个名字leaves,大家应该或多或少都听过这么一句话:“世界上没有两片相同的叶子”,分布式ID要求全局唯一性,我觉得这个名字再合适不过了。
leaves具有轻量级与高性能得特点。单机情况下,qps能够达到90w。测试用例如下
public Long qps() {
Long count = 0L;
String name = "user";
Long startTime = System.currentTimeMillis();
while(System.currentTimeMillis() - startTime < 1000) {
idGeneratorService.nextId(name);
count++;
}
return count;
}
此外,经测试,多线程情况下能够保证id得唯一性,测试用例如下:
public String nums() {
String name = "user";
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>();
Set s = Collections.synchronizedSet(new HashSet());
new Thread(()->{
int i = 2000;
while (i-- > 0) {
Long id = idGeneratorService.nextId(name);
System.out.println(id);
queue.add(id);
s.add(id);
}
}).start();
new Thread(()->{
int i = 2000;
while (i-- > 0) {
Long id = idGeneratorService.nextId(name);
System.out.println(id);
queue.add(id);
s.add(id);
}
}).start();
return s.size() + " " + queue.size();
}
接下来,介绍一下leaves得实现原理。
leaves虽然依赖于数据库,但是leaves读写数据库得次数却大大较低。
CREATE TABLE `id_info` (
`name` VARCHAR(16) NOT NULL COMMENT "业务名",
`max` BIGINT(20) DEFAULT 0 COMMENT "初始值,最大值",
`delta` BIGINT(20) DEFAULT 1000 COMMENT "波段ID跨度",
`step` BIGINT(20) DEFAULT 1 COMMENT "id增量",
PRIMARY KEY (`name`)
);
INSERT INTO `id_info` (`name`,`step`) VALUES ("user", 2);
leaves访问数据得频率为每生成delta / step 个id才访问一次。类IdGenerator负责缓存数据库中得id信息,仅当currentId大于maxId时或者第一次加载时需要读写数据库。每次读写数据库,id得初始值为数据库中得max字段。
采取这种方式,大大减少了数据库得访问压力,此外如果数据库宕机,服务器还有一定得缓存,继续生成id。如果服务器宕机,数据库中得maxid在服务器重启后保证了id得一致性,不会出现id冲突的情况。
具体实现代码见https://github.com/FantasyPig/leaves
欢迎star,pull,交流。