分布式 ID 设计
自己设计
ID 类型
- QPS 高:那么粒度要粗一些
- 粒度小:到达毫秒级,每个毫秒预留 10 位顺序号,所以 QPS 最高达到 1024。每毫秒最多产生 1000 多个 ID。
时间同步
使用 Linux 的 crontab
周期性核准服务器时间:
ntpupdate -u pool.ntp.orgpool.ntp.org
ReentrantLock 生成序列
long sequence = 0;
long lastTimestamp = -1;
Lock = new ReentrantLock();
public void populateId(Id id, IdMeta idMeta) {
lock.lock();
try {
long timestamp = TimeUtils.genTime();
if (timestamp == lastTimestamp) {
sequence++;
sequence &= idMeta.geSeqBitsMask(); // 比如最多 1024 个
if (sequence == 0) {
timestamp = TimeUtils.tillNextTimeUnit(lastTimestamp);
}
} else {
lastTimestamp = timestamp;
sequence = 0;
}
id.setSeq(sequence);
id.setTime(timestamp);
} finally {
lock.unlock();
}
}
序列号用光,就等待进入下一秒:
long tillNextTimeUnit(long lastTimestamp) {
long timestamp = TimeUntils.genTime();
// 自旋,而非 Thread.sleep 来减少线程切换
// 因为我们认为等待的时间不会太长
while (timestamp <= lastTimestamp) {
timestamp = TimeUntils.genTime();
}
return timestamp;
}
CAS 生成序列
class Variant {
private long sequence = 0;
private long lastTimestamp = -1;
}
private AtomicReference<Variant> variant = new AtomicReference<Varient>(new Varient());
public void populateId(Id id, IdMeta idMeta) {
Variant varOld, varNew;
long timestamp, sequence;
while (true) {
varOld = variant.get();
timestamp = TimeUntils.genTime();
sequence = varOld.sequence;
if (timestamp = varOld.lastTimestamp) {
sequence++;
sequence &= idMeta.getSeqBitsMask();
if (sequence == 0) {
timestamp = TimeUntils.tillNextTimeUnit(varOld.lastTimestamp);
}
} else {
sequence = 0;
}
varNew = new Variant();
varNew.sequence = sequence;
varNew.lastTimestamp = timestamp;
if (variant.compareAndSet(varOld, varNew)) {
id.setSeq(sequence);
id.setTime(timestamp);
break;
}
}
}
参考的是 《可伸缩服务架构:框架与中间件》的第一章
Twitter Snowflake
整体长度通常是 64 (1 + 41 + 10+ 12 = 64)位,适合使用 Java 语言中的 long 类型来存储。头部是 1 位的正负标识位。紧跟着的高位部分包含 41 位时间戳,通常使用 System.currentTimeMillis()
。后面是 10 位的 WorkerID
,标准定义是 5
位数据中心 + 5
位机器 ID,组成了机器编号,以区分不同的集群节点。最后的 12
位就是单位毫秒内可生成的序列号数目的理论极限。
基于 Java 的实现有很多:Snowflake。
美团的 Leaf
V1: DB 预分发
DB 之上挂 N 个 Server,每个 Server 启动时,都会去 DB 拿固定长度的 ID List。这样就做到了完全基于分布式的架构,同时因为 ID 是由内存分发,所以也可以做到很高效。接下来是数据持久化问题,Leaf 每次去 DB 拿固定长度的 ID List,然后把最大的 ID 持久化下来,也就是并非每个 ID 都做持久化,仅仅持久化一批 ID 中最大的那一个。
用户通过 Round-robin 的方式调用 Leaf Server 的各个服务,所以某一个 Client 获取到的 ID 序列可能是:1,1001,2001,2,1002,2002……也可能是:1,2,1001,2001,2002,2003,3,4……当某个 Leaf Server 号段用完之后,下一次请求就会从 DB 中加载新的号段,这样保证了每次加载的号段是递增的。
Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit
生成环境的问题:
- 在更新 DB 的时候会出现耗时尖刺,系统最大耗时取决于更新 DB 号段的时间。
- 当更新 DB 号段的时候,如果 DB 宕机或者发生主从切换,会导致一段时间的服务不可用。
V2: 双 Buffer 优化
Leaf 采用了异步更新的策略,同时通过双 Buffer 的方式,保证无论何时 DB 出现问题,都能有一个 Buffer 的号段可以正常对外提供服务,只要 DB 在一个 Buffer 的下发的周期内恢复,就不会影响整个 Leaf 的可用性。
MongoDB 的 ObjectId
提供了一个 12 byte(96 位)的 ID 定义,其中 32 位用于记录以秒为单位的时间,机器 ID 则为 24 位,16 位用作进程 ID,24 位随机起始的计数序列。