分布式唯一ID生成(三):uid-generator

本系列

前言

UidGenerator是Java实现的, 基于Snowflake算法的唯一ID生成器
以组件形式工作在应用项目中,和leaf 不同,leaf是提供server集群,client调server获取ID
有以下特性:

  • 通过特殊的workerId分配策略,解决workerId不好手动指定,以及唯一性问题
  • 通过时钟不回退解决时钟回拨导致的ID重复问题
  • 通过借用未来时间来解决雪花算法天然存在的并发限制

下面依次分析这些特性的实现原理

源码地址:https://github.com/baidu/uid-generator


每一位分配

ID中每一位的分配默认如下:
在这里插入图片描述

  • 最高位:符号位为0,表示是正数
  • 接下来28位:以秒为单位的时间范围,28位的秒能表示8.5年。看起来很少。但如果用cached模式的话这28位和系统时间关系不大,下文详细介绍
  • 接下来22位:workerId,最多支持419w
  • 最低13位:每一秒的序列号,13bit最多支持8192个

雪花算法的难点就是怎么保证ID不重复,分解到每一位中就是保证:

  • 不同的机器一定有不同的workerId,由于代表worker的22位不同,那么不同机器产生的ID一定不同
  • 以及相同的机器产生的所有ID中时间戳不同,由于代表时间戳的28位不同,那么产生的ID一定不同
    • 要做到这点,就要解决时钟回拨问题

怎么保证workerId不重复

workId使用数据库自增主键生成,每次业务实例启动都生成个新的用后即弃,因此永远不会重复
不仅不同机器之间不会重复,相同机器重启后也不会和之前的自己重复
同时也解决了手动配置workerId麻烦的问题

public long assignWorkerId() {  
    // build worker node entity  
    WorkerNodeEntity workerNodeEntity = buildWorkerNode();  
  
    // 往db插一条记录
    workerNodeDAO.addWorkerNode(workerNodeEntity);  
    // 返回主键,当做workerId
    return workerNodeEntity.getId();  
}

只用在db建这张表:其他字段都是描述实例的信息,最关键的就是自增id

CREATE TABLE WORKER_NODE  
(  
	ID BIGINT NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',  
	HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name',  
	PORT VARCHAR(64) NOT NULL COMMENT 'port',  
	TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',  
	LAUNCH_DATE DATE NOT NULL COMMENT 'launch date',  
	MODIFIED TIMESTAMP NOT NULL COMMENT 'modified time',  
	CREATED TIMESTAMP NOT NULL COMMENT 'created time',  
	PRIMARY KEY(ID)  
)  
 COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB;

22位的workerId最多支持419w次启动,如果 每7天重启1次,一年52次,如果按用100年算,能支持接近800台机器。对绝大多数业务来说都够用了
降低重启频率,降低使用年限,以及扩展workerId的bit位,还能支持更多的机器


怎么解决时钟回拨

uid-generator提供了default模式和cached模式,区别在于cached模式能突破每秒qps限制,以及能用更久的时间,下文基于cached模式介绍

uid-generator解决时钟回拨的方法就是,代表时间戳的28位和真实时间没有关系,而是从某一秒开始,直到这个一秒拥有的序列号都用完,才会用下一秒

源码中的开始秒数为:从机器启动开始时间减去2016-5-20对应时间的秒数,但到现在应该已经超过8.5年了,所以需要改改源码。实际上从1开始递增也行

也就是说,这28位只会递增,永远不会回退。和系统时间没关系,也就没有时钟回拨的问题了
但带来的一点小问题就是,生成的ID和时间关联性不大

上一节提到,每个实例每次重启都不会重复,这里再保证每个实例运行期间也不会重复,因此生成的ID保证绝对唯一

怎么突破每秒qps限制

如果是标准雪花算法,每一秒生成的ID数量是有限制的,因为每次获取ID时就要计算当前时间戳,如果当前时间戳的序列号满了,只能等待。不能借用未来的时间戳。因为如果借用了,就可能和未来产生的ID重复

cached模式下,不再每次获取ID时都基于当前时间,而是从一个环形队列中获取
当发现环形队列余量不够时,异步拿下一个时间 lastSecond + 1 的所有序列号填充环形队列,填充完让 lastSecond++
这样还能不能获取下一个ID,就和当前时间没关系了,只和队列中还有没有剩余ID有关系。只要填充速度足够快,就能打破标准雪花算法的每秒qps限制
在这里插入图片描述


获取ID源码如下:

public long take() {  
    // 上一个读到的位置  
    long currentCursor = cursor.get();  
    // 更新读到的位置+1,如果满了不更新  
    long nextCursor = cursor.updateAndGet(old -> old == tail.get() ? old : old + 1);   
  
    // 如果剩下空间少到一定阈值,异步填充  
    long currentTail = tail.get();  
    if (currentTail - nextCursor < paddingThreshold) {  
        bufferPaddingExecutor.asyncPadding();  
    }  
  
    // 满了,拒绝 
    if (nextCursor == currentCursor) {  
        rejectedTakeHandler.rejectTakeBuffer(this);  
    }  
  
    int nextCursorIndex = calSlotIndex(nextCursor);  
    long uid = slots[nextCursorIndex];
    // 返回id
    return uid;
}

异步填充流程:

public void paddingBuffer() {  
    
  
    // is still running  
    if (!running.compareAndSet(false, true)) {  
        LOGGER.info("Padding buffer is still running. {}", ringBuffer);  
        return;  
    }  
  
    // 填充buffer中所有可填充的空间  
    boolean isFullRingBuffer = false;  
    while (!isFullRingBuffer) {  
        // 获取接下来几秒的所有id,直到填满ringBuffer  
        List<Long> uidList = uidProvider.provide(lastSecond.incrementAndGet());  
        for (Long uid : uidList) {  
            isFullRingBuffer = !ringBuffer.put(uid);  
            if (isFullRingBuffer) {  
                break;  
            }  
        }  
    }  
  
    // not running now  
    running.compareAndSet(true, false);  
}

uidProvider.provide的实现如下:

protected List<Long> nextIdsForOneSecond(long currentSecond) {  
    int listSize = (int) bitsAllocator.getMaxSequence() + 1;  
    List<Long> uidList = new ArrayList<>(listSize);  
  
    // 生成这一秒的第一个id,就是填充ID的各个位  
    long firstSeqUid = bitsAllocator.allocate(currentSecond - epochSeconds, workerId, 0L);  
    for (int offset = 0; offset < listSize; offset++) {  
        // 这一秒的其他id根据第一个id计算得到  
        uidList.add(firstSeqUid + offset);  
    }  
  
    return uidList;  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值