Java实现分布式ID生成器

分布式ID的应用背景

在分布式系统中,经常需要对大量的数据、消息、http请求等进行唯一标识,例如:在分布式系统之间http请求需要唯一标识,调用链路分析的时候需要使用这个唯一标识。这个时候数据库自增主键已经不能满足需求,需要一个能够生成全局唯一ID的系统,这个系统需要满足以下需求:

  • 全局唯一:不能出现重复ID。
  • 高可用:ID生成系统是基础系统,被许多关键系统调用,一旦宕机,会造成严重影响。

分布式ID生成方案

1、UUID

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不符合要求。

2、类snowflake方案

据我了解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位,非常灵活。

缺点:

  1. 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

3、数据库生成

以MySQL举例,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。

基于数据的分布式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,交流。

你可能感兴趣的:(Java实现分布式ID生成器)