UidGenerator是Java实现的,提供了两种生成器: DefaultUidGenerator、CachedUidGenerator。

如对UID生成性能有要求, 请使用CachedUidGenerator,支持缓存生成的id。

DefaultUidGenerator的原理是基于Snowflake算法,它使用了时间戳、机器ID和序列号来生成唯一的ID。其中,

时间戳用于保证ID的唯一性和有序性,机器ID用于区分不同的机器,序列号用于解决同一毫秒内并发生成ID的问题。单个实例的QPS能超过6000000。需要的环境:JDK8+,Mysql(用于分配WorkerId)。

优点:1.克服了雪花算法的并发限制,通过借用未来时间来解决squenece天然存在的并发限制。

2. 高性能:支持每秒生成数百万个ID,满足高并发场景的需求,单个实例的QPS能超过6000000。

3. 高可用性:支持多节点部署,即使某个节点宕机也不会影响整个系统的正常运行。

4. 易于使用:提供了简单易用的API,可以快速集成到现有系统中。

5. 可定制化:支持自定义机器ID和序列号的生成方式,可以根据实际需求进行定制。

缺点:1.趋势自增。

2.依赖Mysql做workerId分发,使用Mysql自增Id做workId,用后即弃。

3.UidGenerator的时间部分只有28位,意味着UidGenerator默认只能承受8.5年(2^28-1/86400/365)。

在项目使用的数据库里,执行WORKER_NODE表脚本

DROP TABLE IF EXISTS WORKER_NODE;
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: CONTAINER(1), ACTUAL(2), FAKE(3)',
    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;

maven的pom文件里引入依赖

<dependencies>
    <dependency>
        <groupId>com.github.wujun234</groupId>
        <artifactId>uid-generator-spring-boot-starter</artifactId>
        <version>1.0.3.RELEASE</version>
    </dependency>
</dependencies>

配置文件application.yml里引入自定义配置

# UidGenerator
# 初始时间, 默认:"2019-02-20"
uid:
  epochStr: 2020-05-08
  # 时间位, 默认:30
  timeBits: 41
  # 机器位, 默认:16
  workerBits: 10
  # 序列号, 默认:7
  seqBits: 12
  # 是否容忍时钟回拨, 默认:true
  enableBackward: true
  # RingBuffer size扩容参数, 可提高UID生成的吞吐量, 默认:3
  CachedUidGenerator:
    boostPower: 3
  # 指定何时向RingBuffer中填充UID, 取值为百分比(0, 100), 默认为50
    paddingFactor: 50

IdGenerator.java工具类

@Component
public class IdGenerator {
    @Autowired
    private CachedUidGenerator cachedUidGenerator;

    /**
     * 获取uid
     *
     * @return
     */
    public long nextId() {
        return cachedUidGenerator.getUID();
    }

    /**
     * 格式化传入的uid,方便查看其实际含义
     *
     * @param uid
     * @return
     */
    public String parse(long uid) {
        return cachedUidGenerator.parseUID(uid);
    }
}

源码分析

DefaultUidGenerator

DefaultUidGenerator的产生id的方法与基本上就是常见的snowflake算法实现,仅有一些不同,如以秒为为单位而不是毫秒。

(1)delta seconds(28bits):这个值是指当前时间与epoch时间的时间差,且单位为秒。epoch时间就是集成DefaultUidGenerator生成分布式ID服务第一次上线的时间,可配置,也一定要根据实际上线时间进行配置,因为默认的epoch时间是2016-09-20,不配置的话,会浪费几年的可用时间。

(2) worker id(22bits):DefaultUidGenerator会在集成用它生成分布式ID的实例启动的时候,往表work_node中插入一行数据,得到的id值就是准备赋值给workerId的值。由于workerId默认22位,那么,集成DefaultUidGenerator生成分布式ID的所有实例重启次数是不允许4194303次(即2^22-1),否则会抛出异常。

(3)sequence(23bits):几个实现的关键点。

  a. synchronized保证线程安全。

  b.如果时间有任何的回拨,那么直接抛出异常。

  c.如果当前时间和上一次是同一秒时间,那么sequence自增。如果同一秒内自增值超过2^13-1,那么就会自旋等待下一秒(getNextSecond);

  d.如果是新的一秒,那么sequence重新从0开始。

(4)DefaultUidGenerator的实现可知,它对时钟回拨的处理比较简单粗暴。另外如果使用UidGenerator的DefaultUidGenerator方式生成分布式ID,一定要根据业务的情况和特点,调整各个字段占用的位数。

DefaultUidGenerator的产生id的方法如下。

protected synchronized long nextId() {
    long currentSecond = getCurrentSecond();

    if (currentSecond < lastSecond) {
        long refusedSeconds = lastSecond - currentSecond;
        throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
    }

    if (currentSecond == lastSecond) {
        sequence = (sequence + 1) & bitsAllocator.getMaxSequence();

        if (sequence == 0) {
            currentSecond = getNextSecond(lastSecond);
        }

    } else {
        sequence = 0L;
    }
    lastSecond = currentSecond;

    return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
}

nextId方法主要负责ID的生成,这种实现方式很简单,如果毫秒数未发生变化,在序列号加一即可,毫秒数发生变化,重置Sequence为0。

CachedUidGenerator

CachedUidGenerator是DefaultUidGenerator的重要改进实现。核心利用了RingBuffter,它本质上是一个数组,数组中每个项被称为slot。CachedUidGenerator设计了两个RingBuffer,一个保存唯一ID,一个保存flag。RingBuffer的尺寸是2^n,n必须是正整数。

采取的措施和方案规避时钟回拨问题和增强唯一性

(1) 自增列:CachedUidGenerator的workerId在实例每次重启时初始化,且就是数据库的自增ID,从而完美的实现每个实例获取到的workerId不会有任何冲突;

(2)RingBuffer:CachedUidGenerator不再每次取ID时都实时计算分布式ID,而是利用RingBuffer数据结构预先生成若干分布式ID并保存。

(3)时间递增:传统的SnowFlake算法实现都是实现System.currentYimeMills()来获取时间并与上一次时间进行比较,这样的实现严重依赖服务器的时间。而CachedUidGenerator的时间类型是AtomicLong,且通过incrementAndGet()方法获取下一次时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题。

(4)CachedUidGenerator通过缓存的方式预先生成一批唯一ID列表,可以解决唯一ID获取时候的耗时。但这种方式,一方面需要耗费内存来缓存这部分数据,另外如果访问量不大的情况下,提前生成的UID中的时间戳可能是很早之前的。

基本实现原理

正如名字体现的那样,这是一种缓存型的ID生成方式,当剩余ID不足的时候,会异步的方式重新生成一批ID缓存起来,后续请求的时候直接的时候直接返回现成的ID即可。

在实现上, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题. 最终单机QPS可达600万。

使用RingBuffer缓存生成的id。RingBuffer是个环形数组,默认大小为8192个,里面缓存着生成的id。

CachedUidGenerator采用了双RingBuffer,Uid-RingBuffer用于存储Uid、Flag-RingBuffer用于存储Uid状态(是否可填充、是否可消费)