分布式Id/框架/发号器一文介绍

文章目录

一、分布式id介绍

1、什么是分布式id

业务需求中需要对各种数据使用id进行唯一标识。

分布式ID是在分布式系统下使用的唯一标识,它用于保证数据的一致性和唯一性。

在传统的单机环境下,可以使用自增主键或UUID来生成唯一的ID,但在分布式环境下,由于多个节点同时生成ID,可能会出现重复的情况。

因此,需要一种能够生成全局唯一ID的解决方案,这就是分布式ID。

2、分布式id的特点

  • 全局唯一:在分布式环境下无论是在哪个节点生成的ID,都必须是全局唯一的,不能出现重复的情况
  • 趋势递增:生成的分布式ID通常要求能够近似有序递增,有助于后续业务需求使用id记性排序。ID 的有序性可以提升数据库写入速度,如在数据库中使用B+Tree数据结构时,自增顺序写入的数据能够保持B+Tree的结构稳定,从而提高存取效率
  • 高可用性:分布式ID生成系统需要保证在任何时候都能正确生成ID,即使在某些节点故障的情况下,整个系统仍能保持正常运行
  • 高性能性:在分布式环境下,特别是在高并发场景下,分布式ID生成系统需要能够迅速、高效地生成ID,且对服务本身的本地资源消耗小,以满足业务需求。
  • 信息安全:分布式ID的生成过程需要保证安全,不能被恶意用户推测出下一个ID,以防止可能的攻击,比如获取订单号,获取下载链接等

二、UUid生成算法

1、JDK UUID

JDK提供了现成的UUID生成方式,如下代码所示,包含了32个16进制的数字,以-分为五段,8-4-4-4-12

java 复制代码
UUID uuid = UUID.randomUUID();
//输出示例:5c89a67d-4541-42d1-85e9-807bd93f065b
System.out.println(uuid);

JDK的版本不同,生成UUID的规则也不同,实现方式参考文档:
从源码角度分析UUID的实现原理

  • 版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成;
  • 版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成;
  • 版本 3、版本 5 : 通过散列(hashing)名字空间(namespace)标识符和名称生成;
  • 版本 4 : UUID 使用随机性open in new window或伪随机性open in new window生成

优点 :生成速度快,简单易用,一行代码搞定,本地生成,没有网络损耗
缺点

  1. 存储空间消耗大: UUID 是一个 128 位的字符串,相比于传统的 32 位或 64 位整数 ID,它占用了更多的存储空间。这可能会增加数据库和存储系统的负担
  2. 可读性差: UUID 的格式通常是一长串的字符,可读性差
  3. 无序: 基于时间的 UUID 虽然具有一定的有序性,但由于其包含了时间戳、机器 MAC 地址和随机数等多个部分,因此并不完全按照时间顺序排列。这可能对于某些需要按时间顺序处理数据的场景造成不便

2、Snowflake 雪花算法

Snowflake算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个64 bits的唯一ID(long)。默认采用上图字节分配方式,除了sign:1bit序号位不可调整外,其他的标识都可以根据实际情况做些调整。

  • sign(1bit):固定1bit符号标识,表示正式,业务中常用于标识生成的UID为正数
  • timestamp(41bit) :用于表示时间戳,相对于某个时间基点的增量值,单位是毫秒,可以支持 (1L<<41)/1000L360024*365 ≈ 69年
  • datacenter id + worker id (10bit):在分布式IDC环境下,一般用前5位标识机房id/大区id,后五位表示机器id,用来区分不同的集群/机房的节点,这样可以表示32个IDC,每个IDC下面可以有32个机器
  • sequence(12bit):用来表示每毫秒下的并发序列,序列号为自增值,12个bit代表单台机器每毫秒能够生成2^12=4096个ID,理论上snowflake方案的QPS约为409.6w/s
  • 可支持单业务单大区重启 2^17-1=131071次

优点 :生成速度比较快,毫秒数在高位,自增序列号在低位,整体ID是趋势递增的,业务需要时可以加上业务ID
缺点:强依赖机器时钟,如果机器上时钟回拨(即服务器上的时间突然倒退回之前的时间),进而导致发号重复或者服务会处于不可用状态

3、PearFlower 梨花算法

雪花算法的改进方法,目标:解决雪花算法的时间回拨问题,秒级时间戳回避毫秒级时间回拨问题;随机段号降低相同机器ID造成的影响
原理

  • 符号位:1位
  • 时间:31位,精确到秒级,可以用68年
  • 段号(批次号):3位 每秒可分为8个段
  • 机器号:10位 最多支持1024台机器
  • sequence:19位 可表示:0--524287

优化点

  1. 经过调整,时间只对秒灵敏,成功回避了服务器间几百毫秒的时间误差引起的时间回拨问题;
  2. 若第59秒的8个段号没有用完,则当润秒来临时,还可继续使用
  3. 可设置一定的秒数(如3秒)内提前消费。比如第10秒的号码,在800毫秒用完了,可以继续使用第11秒的号码。这样,下1秒用的号码不是很多时,就可以借给上1秒使用。

参考:分布式全局唯一ID生成算法(改进的雪花算法------解决时钟回拨问题) https://blog.csdn.net/abckingaa/article/details/106460583

4、Mist 薄雾算法


原理

  • 第一段为最高位,占 1 位,保持为 0,使得值永远为正数;
  • 第二段放置自增数,占 47 位,自增数在高位能保证结果值呈递增态势,遂低位可以为所欲为;
  • 第三段放置随机因子一,占 8 位,上限数值 255,使结果值不可预测;
  • 第四段放置随机因子二,占 8 位,上限数值 255,使结果值不可预测;

优点:递增态势、不依赖数据库、高性能;不受时间回拨的影响

缺点:程序重启会造成按序递增数值回到初始值,要借助存储在程序重启时恢复自增序列。

三、常见发号器服务

1、数据库

1)自增

原理:利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增

  1. 建表:
sql 复制代码
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=InnoDB DEFAULT CHARSET=utf8mb4;

创建一张序号表,stub字段无实际意义,只是为了占位,便于我们插入或者修改数据。并且,给 stub 字段创建了唯一索引,保证其唯一性。

  1. SQL
sql 复制代码
BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT LAST_INSERT_ID();
COMMIT;


DELETE FROM sequence_id WHERE id = LAST_INSERT_ID();

第一步:尝试把数据插入表中

第二步:如果主键或唯一索引的字段因出现重复数据而插入失败时,先从表中删除含有重复关键字的冲突行,然后再次尝试把数据插入表中。

优点

  1. 非常简单,利用现有数据库系统的功能实现,成本小,存储空间消耗小,有DBA专业维护
  2. ID号单调自增,可以实现一些对ID有特殊要求的业务

缺点

  1. 支持的并发量不大, 强依赖DB,当DB异常时整个系统不可用,属于致命问题,配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证,主从切换时的不一致可能会导致重复发号。
  2. 存在数据库单点问题,ID发号性能瓶颈限制在单台MySQL的读写性能 ,可以用数据库集群解决,不过增加了系统复杂度。如在分布式系统中每台机器设置不同的id初始值,且步长和机器数相等,不过系统维护困难,系统水平扩展比较困难,比如定义好了步长和机器台数之后,不容易扩展
  3. 数据库压力大,每次获取ID都得读写一次数据库,只能靠堆机器来提高性能
  4. ID没有具体的业务含义,而且存在安全问题,自增的订单ID存在规律,可以推测出业务数据,比如每天订单量等

2)号段模式

原理

从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将使用本号段,生成1~1000的自增ID并加载到内存,从内存获取速度很快

  1. 建表
sql 复制代码
CREATE TABLE `sequence_id_generator` (
  `id` int(10) NOT NULL,
  `current_max_id` bigint(20) NOT NULL COMMENT '当前最大id',
  `step` int(10) NOT NULL COMMENT '号段的长度',
  `version` int(20) NOT NULL COMMENT '版本号',
  `biz_type`    int(20) NOT NULL COMMENT '业务类型',
   PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

biz_type :代表不同业务类型

current_max_id:当前最大的可用id

step :代表号段的长度

version :是一个乐观锁,每次都更新version,保证并发时数据的正确性

current_max_id 字段和step字段主要用于获取批量 ID,获取的批量 id 为:current_max_id ~ current_max_id+step。

  1. 先新增一行数据
sql 复制代码
INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`)
VALUES
 (1, 0, 100, 0, 101);
  1. 通过 SELECT 获取指定业务下的批量唯一 ID
sql 复制代码
UPDATE sequence_id_generator SET current_max_id = current_max_id +step, version=version+1 WHERE version = 0  AND `biz_type` = 101
SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101

结果:id current_max_id step version biz_type

1 100 100 1 101

  • update current_max_id = current_max_id + step,update成功则说明新号段获取成功,新的号段范围是(current_max_id ,max_id +step]
  • update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX
  • 由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多
  1. 不够用的话,更新之后重新 SELECT 即可。
    等这批号段ID用完,再次向数据库申请新号段,对current_max_id 字段做一次update操作

优点:比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小,存储消耗空间更小

缺点:跟自增模式类型,同样存在数据库单点问题、ID 没有具体业务含义、安全问题、

2、NoSQL

以使用Redis为主,原理就是利用redis的 incr命令实现ID的原子性自增。

bash 复制代码
127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id      // 增加1,并返回递增后的数值
(integer) 2

要考虑到redis持久化的问题,避免重启机器或者机器故障后数据丢失。redis有两种持久化方式RDB和AOF。

  • RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
  • AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。

优点:通过Redis的INCR/INCRBY自增原子操作命令,能保证生成的ID肯定是唯一有序的,本质上实现方式与数据库一致,整体吞吐量比数据库要高

缺点:跟数据库自增模式的缺点类型,因为强依赖存储,Redis实例或集群宕机后服务依然不可用。

四、常见框架

1、百度UIDGenerator

百度的框架是基于雪花算法的,官方文档:UidGenerator 的官方介绍

  • sign(1bit):固定1bit符号标识,即生成的UID为正数。
  • delta seconds (28 bits):当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约8.7年
  • worker id (22 bits):机器id,最多可支持约420w次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。
    -sequence (13 bits):每秒下的并发序列,13 bits可支持每秒8192个并发。

2、美团Leaf

官方文档:Leaf------美团点评分布式ID生成系统

提供了 号段模式 和 Snowflake(雪花算法) 这两种模式来生成分布式 ID。

2.1、Leaf-Segment 数据库号段

原理如上述发号器服务模式的 数据库号段模式类似。在使用原有数据库号段的方案上,做了一些改变,各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响

如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

优点

  • Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景
  • ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求
  • 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务
  • 可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来

缺点

  • ID号码不够随机,能够泄露发号数量的信息,不太安全
  • TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺
  • DB宕机会造成整个系统不可用

双buffer优化

对于第二个缺点,Leaf服务作出了双buffer优化方案,服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。避免获取 DB 在获取号段的时候阻塞请求获取 ID 的线程。简单来说,就是我一个号段还没用完之前,我自己就主动提前去获取下一个号段

当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复

每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响

每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新

2.2、Leaf-Snowflake 雪花

完全沿用雪花算法的bit位设计"1+41+10+12"组装ID号。

对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置;服务规模较大,使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。

启动方案

  1. 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)
  2. 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务
  3. 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务

弱依赖ZooKeeper

除了每次会去ZK拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。这样做到了对三方组件的弱依赖。一定程度上提高了SLA

解决时钟问题

服务启动时首先检查自己是否写过ZooKeeper leaf_forever节点

  1. 若写过,则用自身系统时间与leaf_forever/{self}节点记录时间做比较,若小于leaf_forever/{self}时间则认为机器时间发生了大步长回拨,服务启动失败并报警
  2. 若未写过,证明是新服务节点,直接创建持久节点leaf_forever/${self}并写入自身系统时间,接下来综合对比其余Leaf节点的系统时间来判断自身系统时间是否准确,具体做法是取leaf_temporary下的所有临时节点(所有运行中的Leaf-snowflake节点)的服务IP:Port,然后通过RPC请求得到所有节点的系统时间,计算sum(time)/nodeSize。
  3. 若abs( 系统时间-sum(time)/nodeSize ) < 阈值,认为当前系统时间准确,正常启动服务,同时写临时节点leaf_temporary/${self} 维持租约
  4. 否则认为本机系统时间发生大步长偏移,启动失败并报警
  5. 每隔一段时间(3s)上报自身系统时间写入leaf_forever/${self}

由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警

3、滴滴TinyId

滴滴开源的一款基于数据库号段模式的ID生成器,方案也类似Leaf-Segment,具体文档参考:tinyid官方原理介绍

通过 HTTP 请求向发号器服务申请唯一 ID。负载均衡 router 会把我们的请求送往其中的一台 tinyid-server

相比于基于数据库号段模式的简单架构方案,Tinyid 方案主要做了下面这些优化:

  1. 双号段缓存:为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。 Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
  2. 增加多 db 支持:支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。
  3. 增加 tinyid-client:纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。

五、Java实战

模拟Leaf-Snowflake,改为利用redis分配workId

  • sign(1bit):固定1bit符号标识,即生成的UID为正数。
  • delta seconds (31 bits):当前时间,相对于时间基点的增量值,单位:秒,可用2^31-1=2147483647/86400/365=68.1年
  • region id(2 bit):大区id ,区分欧美亚三个大区
  • worker id (17 bits):机器id ,可支持单业务单大区重启 2^17-1=131071次
  • sequence (13 bits):每秒下的并发序列,13 bits可支持每秒8192个并发。

优化点:

  1. 使用更加常用的redis分配workid,服务更加轻量级
  2. 自定义起始时间,理论上服务没有使用年限限制
  3. 服务内部判断时间回拨,判断序列号使用完,若使用完则等待至下一秒,使用下一秒的序列号

示例代码
1、分配workerId

java 复制代码
@Slf4j
public class RedisWorkerIdAssigner implements WorkerIdAssigner {

    private RedisTemplate<String, String> redis;

    @Autowired
    private Environment environment;
    
    public RedisWorkerIdAssigner(RedisTemplate<String, String> redis) {
        this.redis = redis;
    }

    @Override
    public long assignWorkerId(WorkerIdAssignerProperties workerIdAssignerParam) {
        String ip = NetUtils.getLocalAddress();
        String port = environment.getProperty("server.port");
        log.info("ip:{},port:{}", ip, port);
        WorkerNodeInfo workerNodeInfo = WorkerNodeInfo.builder()
                .ip(ip)
                .port(port)
                .build();
        log.info("workerNodeInfo:{}", workerNodeInfo);
        long machineId = 0;
        if (null == ip || redis == null) {
            log.warn("can not get localhost");
            /**
             * 抛异常
             */
            throw new IdGenerateException("can not get localhost ip");

        } else {
            boolean getLock = false;
            int count = 0;
            // 不断尝试获取锁 最多尝试15次 30S
            String idKeyLock = SpiderIdKey.getLockKey(workerNodeInfo, workerIdAssignerParam);

            while (!getLock) {
                getLock = redis.opsForHash().increment(idKeyLock, "LOCK", 1) == 1;
                if (!getLock) {
                    try {
                        Thread.sleep(500);
                    } catch (Exception e) {

                    }
                    count++;
                    log.info("IdConfig initIdService message : try to get lock from redis... ");
                    if (count > 15) {
                        throw new IdGenerateException("can not get Lock from redis");
                    }
                }


            }
            if (getLock) {
                // 查询本机是否有值
                String workerIdKey = SpiderIdKey.getWorkerIdKey(workerNodeInfo, workerIdAssignerParam);
                String redisValue = redis.opsForValue().get(workerIdKey);
                if (null == redisValue) {
                    // 本机无值 遍历hash value 赋予新值
                    Long now = redis.opsForValue().increment(SpiderIdKey.getBizWorkerIncKey(workerIdAssignerParam));
                    redis.opsForValue().set(workerIdKey, String.valueOf(now));
                    machineId = now;
                } else {
                    // 本机有值  即可能为服务重启
                    machineId = Long.parseLong(redisValue);
                }
                if (getLock) {
                    expire(idKeyLock, 1L);
                }
            }

        }
        return machineId;
    }


    public boolean expire(String key, Long expireTime) {

        return Optional.ofNullable(redis.expire("#hash:" + key, expireTime, TimeUnit.SECONDS)).orElse(false);
    }

}

2、获取ID:

java 复制代码
@Slf4j
public class WebLiveCommonDefaultIdGenerator implements IdGenerator, InitializingBean {
    /**
     *
     *
     * Bits allocate
     */
    /**
     * 默认可以使用68年
     * 2^31-1=2147483647/86400/365=68.1
     */
    protected int timeBits = 31;
    protected int regionBits = 2;

    /**
     * 2^17=131072-1=131071;
     */
    protected int workerBits = 17;
    /**
     * 2^13=8192
     */
    protected int seqBits = 13;
    protected WorkerIdAssignerProperties workerIdAssignerProperties;

    /**
     * Customer epoch, unit as second. For example 2016-05-20 (ms: 1463673600000)
     * <p>
     * 开始时间
     */
    protected String epochStr = "2023-04-04";
    protected long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseByDayPattern(epochStr).getTime());

    /**
     * Stable fields after spring bean initializing
     */
    protected BitsAllocator bitsAllocator;
    protected long workerId;

    protected long regionId;

    /**
     * Volatile fields caused by nextId()
     */
    protected long sequence = 0L;
    protected long lastSecond = -1L;

    protected WorkerIdAssigner workerIdAssigner;


    private WebLiveCommonAlarmService webLiveCommonAlarmService;

    @Override
    public void afterPropertiesSet() throws Exception {
        // initialize bits allocator
        bitsAllocator = new BitsAllocator(timeBits, regionBits, workerBits, seqBits);
        String bizTag = workerIdAssignerProperties.getBizTag();
        if (StringUtils.isBlank(bizTag)) {
            throw new IdGenerateException("bizTag is null");
        }
        //bizTag 长度不超过40
        if (bizTag.length() > 40) {
            throw new IdGenerateException("bizTag is too long");
        }
        //  bizTag正则表达式校验,字母数字下划线中划线    ^[a-zA-Z0-9_-]+$
        if (!bizTag.matches("^[a-zA-Z0-9_-]+$")) {
            throw new IdGenerateException("bizTag is not match regex ^[a-zA-Z0-9_-]+$");
        }
        if (Objects.nonNull(workerIdAssignerProperties.getRegionId())) {
            regionId = workerIdAssignerProperties.getRegionId();
        }
        if (Objects.nonNull(workerIdAssignerProperties.getWorkerBits())) {
            workerBits = workerIdAssignerProperties.getWorkerBits();
        }
        if (Objects.nonNull(workerIdAssignerProperties.getSeqBits())) {
            seqBits = workerIdAssignerProperties.getSeqBits();
        }
        if (Objects.nonNull(workerIdAssignerProperties.getTimeBits())) {
            timeBits = workerIdAssignerProperties.getTimeBits();
        }
        if (Objects.nonNull(workerIdAssignerProperties.getRegionBits())) {
            regionBits = workerIdAssignerProperties.getRegionBits();
        }
        if (regionId < 0 || workerBits < 0 || seqBits < 0 || timeBits < 0 || regionBits < 0) {
            throw new IdGenerateException("regionId,workerBits,seqBits,timeBits,regionBits must be positive");
        }
        if (StringUtils.isNotBlank(workerIdAssignerProperties.getEpochStr())) {
            epochStr = workerIdAssignerProperties.getEpochStr();
            epochSeconds = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseByDayPattern(epochStr).getTime());
            //校验epochSeconds不能大于当前时间        epochSeconds<当前时间
            if (epochSeconds > TimeUnit.MILLISECONDS.toSeconds(new Date().getTime())) {
                throw new IdGenerateException("epochSeconds is bigger than current time");
            }

        }


        if (regionId == 0) {
            regionId = ServerInfoUtil.getRegionCode();
        }
        workerIdAssignerProperties.setRegionId(regionId);
        workerId = workerIdAssigner.assignWorkerId(workerIdAssignerProperties);
        if (workerId > bitsAllocator.getMaxWorkerId()) {
            // 告警一下吧 然后重新分配
            webLiveCommonAlarmService.errorMultiAlarm(60, 1001, "bitsAllocator",
                    "bitsAllocator over max worker id,workId:{},bizTag:{}", workerId, bizTag);
            log.warn("bitsAllocator over max worker id,workId:{},bizTag:{}", workerId, bizTag);
            workerId = workerId % bitsAllocator.getMaxWorkerId();

        }
        log.info("Initialized bits(1,{}, {}, {}, {}) for workerID:{}", timeBits, regionBits, workerBits, seqBits, workerId);


    }


    /**
     * Setters for spring property
     */
    public void setWorkerIdAssigner(WorkerIdAssigner workerIdAssigner) {
        this.workerIdAssigner = workerIdAssigner;
    }

    public void setWebLiveCommonAlarmService(WebLiveCommonAlarmService webLiveCommonAlarmService) {
        this.webLiveCommonAlarmService = webLiveCommonAlarmService;
    }

    public long getNextId() throws IdGenerateException {
        try {
            return nextId();
        } catch (Exception e) {
            throw new IdGenerateException(e);
        }
    }

    /**
     * Get UID
     *
     * @return UID
     * @throws IdGenerateException in the case: Clock moved backwards; Exceeds the max timestamp
     */
    protected synchronized long nextId() {
        long currentSecond = getCurrentSecond();
        // Clock moved backwards, refuse to generate uid
        if (currentSecond < lastSecond) {
            long refusedSeconds = lastSecond - currentSecond;
            throw new IdGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
        }

        // At the same second, increase sequence
        if (currentSecond == lastSecond) {
            sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
            // Exceed the max sequence, we wait the next second to generate uid
            if (sequence == 0) {
                currentSecond = getNextSecond(lastSecond);
            }

            // At the different second, sequence restart from zero
        } else {
            sequence = 0L;
        }

        lastSecond = currentSecond;

        // Allocate bits for UID
        return bitsAllocator.allocate(currentSecond - epochSeconds, regionId, workerId, sequence);
    }

    /**
     * Get next millisecond
     */
    private long getNextSecond(long lastTimestamp) {
        long timestamp = getCurrentSecond();
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentSecond();
        }

        return timestamp;
    }

    /**
     * Get current second
     */
    private long getCurrentSecond() {
        long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
        if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
            throw new IdGenerateException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
        }

        return currentSecond;
    }


    public void setWorkerIdAssignerProperties(WorkerIdAssignerProperties workerIdAssignerProperties) {
        this.workerIdAssignerProperties = workerIdAssignerProperties;
    }

    @Override
    public String parseID(long id) {
        long totalBits = BitsAllocator.TOTAL_BITS;
        long signBits = bitsAllocator.getSignBits();
        long timestampBits = bitsAllocator.getTimestampBits();
        long regionIdBits = bitsAllocator.getRegionIdBits();
        long workerIdBits = bitsAllocator.getWorkerIdBits();
        long sequenceBits = bitsAllocator.getSequenceBits();

        // parse UID
        long sequence = (id << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
        long workerId = (id << (timestampBits + signBits + regionIdBits)) >>> (totalBits - workerIdBits);
        long regionId = (id << (timestampBits + signBits)) >>> (totalBits - regionIdBits);

        long deltaSeconds = id >>> (workerIdBits + regionIdBits + sequenceBits);

        Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
        String thatTimeStr = DateUtils.formatByDateTimePattern(thatTime);

        // format as string
        return String.format("{\"WebLiveCommonId\":\"%d\",\"timestamp\":\"%s\",\"regionId\":\"%d\",\"workerId\":\"%d\",\"sequence\":\"%d\"}",
                id, thatTimeStr, regionId, workerId, sequence);
    }
}


public class BitsAllocator {
    /**
     * Total 64 bits
     */
    public static final int TOTAL_BITS = 1 << 6;

    /**
     * Bits for [sign-> second-> workId-> sequence]
     */
    private int signBits = 1;
    private final int timestampBits;
    private final int regionIdBits;
    private final int workerIdBits;
    private final int sequenceBits;

    /**
     * Max value for workId & sequence
     */
    private final long maxDeltaSeconds;
    private final long maxWorkerId;
    private final long maxSequence;

    /**
     * Shift for timestamp & workerId
     */
    private final int timestampShift;
    private final int regionIdShift;

    private final int workerIdShift;

    /**
     * Constructor with timestampBits, workerIdBits, sequenceBits<br>
     * The highest bit used for sign, so <code>63</code> bits for timestampBits, workerIdBits, sequenceBits
     */
    public BitsAllocator(int timestampBits, int regionIdBits, int workerIdBits, int sequenceBits) {
        // make sure allocated 64 bits
        int allocateTotalBits = signBits + timestampBits + regionIdBits + workerIdBits + sequenceBits;
        Assert.isTrue(allocateTotalBits == TOTAL_BITS, "allocate not enough 64 bits");
        // initialize bits
        this.timestampBits = timestampBits;
        this.regionIdBits = regionIdBits;
        this.workerIdBits = workerIdBits;
        this.sequenceBits = sequenceBits;

        // initialize max value
        this.maxDeltaSeconds = ~(-1L << timestampBits);
        this.maxWorkerId = ~(-1L << workerIdBits);
        this.maxSequence = ~(-1L << sequenceBits);

        // initialize shift
        this.timestampShift = workerIdBits + sequenceBits + regionIdBits;
        this.regionIdShift = workerIdBits + sequenceBits;
        this.workerIdShift = sequenceBits;
    }

    /**
     * Allocate bits for UID according to delta seconds & workerId & sequence<br>
     * <b>Note that: </b>The highest bit will always be 0 for sign
     *
     * @param deltaSeconds
     * @param workerId
     * @param sequence
     * @return
     */
    public long allocate(long deltaSeconds, long regionId, long workerId, long sequence) {
        return (deltaSeconds << timestampShift) | (regionId << regionIdShift) | (workerId << workerIdShift) | sequence;
    }

    /**
     * Getters
     */
    public int getSignBits() {
        return signBits;
    }

    public int getTimestampBits() {
        return timestampBits;
    }

    public int getWorkerIdBits() {
        return workerIdBits;
    }

    public int getSequenceBits() {
        return sequenceBits;
    }

    public int getRegionIdBits() {
        return regionIdBits;
    }

    public long getMaxDeltaSeconds() {
        return maxDeltaSeconds;
    }

    public long getMaxWorkerId() {
        return maxWorkerId;
    }

    public long getMaxSequence() {
        return maxSequence;
    }

    public int getTimestampShift() {
        return timestampShift;
    }

    public int getWorkerIdShift() {
        return workerIdShift;
    }

    public int getRegionIdShift() {
        return regionIdShift;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}
相关推荐
青山木8 分钟前
一把 Redis 分布式锁,踩透四个坑:锁争抢、僵尸锁、锁过期、锁丢失
java·数据库·redis·后端
Java内核笔记30 分钟前
万字避坑!Spring Boot 3.x 升 4.0 最全升级指南(附 Migration Checklist,建议收藏⭐)
java·后端
衣乌安、1 小时前
Redis 分布式锁与任务状态
数据库·redis·分布式
cfm_29141 小时前
SpringBoot启动原理
java·spring boot·spring
漫随流水2 小时前
Java——springboot web案例
java·开发语言·spring boot
从此以后自律2 小时前
Java Object 类常用方法全讲解
java·开发语言
念何架构之路2 小时前
Docker 容器状态机 学习
java·学习·docker
你有我备注吗3 小时前
SQL学习之查询
java·sql·学习
xixi09243 小时前
JMeter5.6.3基础使用教程1
java·开发语言
带刺的坐椅3 小时前
用 Solon ReActAgent 落地发票识别与智能报销
java·llm·solon·react-agent