百度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());
相同的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。