唯一ID生成器

499 阅读4分钟

百度uid-generator

百度的ID生成器基于snowflake算法实现。

DB表结构

DROP DATABASE IF EXISTS `xxxx`;
CREATE DATABASE `xxxx` ;
use `xxxx`;
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: 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;

worker-id生成

worker-id的生成基于DB的自增ID实现。如果同一个实例重启,那么该worker-id就会自增一次。

INSERT INTO WORKER_NODE(HOST_NAME,PORT,TYPE,LAUNCH_DATE,MODIFIED,CREATED) values("127.0.0.1","8888",1,NOW(),NOW(),NOW());

图片.png

相同的host port其ID自增。

就像代码注释中一样:the worker id will be discarded after assigned to the UidGenerator

该worker-id一旦分配给一个UidGenerator实例,那么这个worker-id后面就被废弃了。

百度实现的uid很耗费worker-id

百度分配的worker-id占用了22位,最多可支持约420w次机器启动。

DefaultUidGenerator

先看下默认实现存在的一个问题,代码如下:

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

    // 考虑了时钟回退的问题
    // Clock moved backwards, refuse to generate uid
    if (currentSecond < lastSecond) {
        long refusedSeconds = lastSecond - currentSecond;
        throw new UidGenerateException("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) {
            // 此处极端情况下要阻塞1s
            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, workerId, sequence);
}

/**
 * Get next millisecond
 */
// 极端情况下此处要阻塞近1s
// 由于while循环,特别消耗CPU资源
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 UidGenerateException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
    }

    return currentSecond;
}

上面默认实现存在两个主要问题:

  • synchronized锁粒度比较大
  • 在同一秒内如果QPS比较高,很快就会消耗光sequence(默认最大8096个),导致长时间的while循环,不仅消耗CPU资源,而且导致长时间无法释放锁。

CachedUidGenerator

百度的实现一大亮点是基于RingBuffer减少了加锁的上下文切换,提升了性能。

  • 获取id时不用加锁了
  • sequence耗尽的时候无需空等待了,可以透支未来的时间,并支持异步生成新的uid
  • uid缓存不再是简单的缓存某一秒的id列表,可以支持更多的id缓存列表(默认是8倍的sequence)

存在的问题

  • 使用RingBuffer会透支未来的时间,如果该实例宕掉,又立马恢复,那么有可能存在生成重复的id。
  • 例如:
    • 20220720-14:00:00消耗到了20220720-14:00:05秒的id
    • 20220720-14:00:01放生重启
    • 重启后初始化的RingBuffer可能是20220720-14:00:03的id
    • 导致了2s的id重复
  • 一点都没有考虑时钟回退的问题

美团leaf

美团提供了两种是实现:基于DB的和基于snowflake的。

snowflake

worker-id生成

基于zk的持久有序节点实现。

启动时优先判断该实例的节点是否存在,如果存在那么使用之前的序号作为worker-id;否则,创建新的zk节点,产生新的序号作为worker-id。

美团在实现时,弱依赖了zk:在启动时,如果zk不可用,从本地文件读取worker-id。如果该实例没有被启动过,是一个全新的实例,在启动时,zk不可用,本地文件也不存在,那么就会启动失败。

优势

解决了小范围时钟(小于5ms)回退的问题。

// synchronized锁
public synchronized Result get(String key) {
    long timestamp = timeGen();
    if (timestamp < lastTimestamp) {
        long offset = lastTimestamp - timestamp;
        // 时钟回退小于5ms
        if (offset <= 5) {
            try {
                wait(offset << 1);
                timestamp = timeGen();
                if (timestamp < lastTimestamp) {
                    return new Result(-1, Status.EXCEPTION);
                }
            } catch (InterruptedException e) {
                LOGGER.error("wait interrupted");
                return new Result(-2, Status.EXCEPTION);
            }
        } else {
            return new Result(-3, Status.EXCEPTION);
        }
    }
    if (lastTimestamp == timestamp) {
        sequence = (sequence + 1) & sequenceMask;
        if (sequence == 0) {
            //seq 为0的时候表示是下一毫秒时间开始对seq做随机
            sequence = RANDOM.nextInt(100);
            timestamp = tilNextMillis(lastTimestamp);
        }
    } else {
        //如果是新的ms开始
        sequence = RANDOM.nextInt(100);
    }
    lastTimestamp = timestamp;
    long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence;
    return new Result(id, Status.SUCCESS);

}

基于DB的号段模式

MySQL库表结构

CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128)  NOT NULL DEFAULT '',
  `max_id` bigint(20) NOT NULL DEFAULT '1',
  `step` int(11) NOT NULL,
  `description` varchar(256)  DEFAULT NULL,
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')

基于DB的号段模式

采用了双buffer的方式异步更新buffer。

记录当前的maxId和步长。当申请的buffer的id value超过一定阈值(maxId与当前id的差值)的时候,则异步加载另一个buffer。

参考