一般说起分布式id,就不得不提到雪花Id-SnowflakeId。
作为一种去中心,具有递增性质,且不会暴露真实业务量级的方案,它将64位二进制划分成1+41时间戳+10机器id+12序列号,不得不赞叹其大道至简。
一、核心痛点:时钟回拨的解决方案对比
背过八股文的我们都知道,雪花ID的最大问题就在于时钟回拨,有可能导致生成的id重复,这对于大型公司自然是不可以接受的。因此,像是美团、百度、滴滴就开源了自己的分布式id方案,本文主要讨论雪花id变体和其相关的问题,对于号段模式不做讨论。
1. 美团 Leaf-snowflake:等待追平 + 异常熔断
在美团开源的方案 Leaf-snowflake中,对于时钟问题的建议处理方案是,记录最后生成id的时间,每次生成时先进行比较,如果相差不大,那么sleep到新时间之后再生成;否则,报警并下掉该节点。下面的代码是leaf项目中的源码。
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
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中未生成完的序列id。这样当发生时钟回拨时,先判断上次的序列是否达到上限,没有的话依然使用上次的时间戳继续生成。直到达到上限后再去进行追平或报警。可能差别不是很大,但我个人倾向于可以先用再追平。
2.百度 UidGenerator:两种模式适配不同场景
在百度的方案UidGenerator中,
DefaultUidGenerator的思路是等待时间追平后再生成id,和美团类似。
CachedUidGenerator则是通过预支未来的时间,其核心是启动时取当前时间的秒级时间戳,之后通过long的cas自增操作进行+1(不依赖系统的时间)–类比单机从1开始,然后做自增操作。不过这里的起始值是具有时间属性的。而为了存储预支的时间,设计了两个buffer来维护。一个用于存储id,一个用于判断是否可以存储Id。
CachedUidGenerator 将「时间依赖」转移到启动阶段,运行时完全脱离系统时间,既保证了趋势递增,又从根源上解决了时钟回拨导致的 ID 重复问题。
二、关键细节:WorkerId 的分配与复用
百度的UIdGenerator为了保证workId的自增和统一管理,默认是利用db来进行存储(当然也可以通过其他的方式,实现WorkerIdAssigner接口),使用自增的id主键来作为workId,目前是不会重复使用workId,因为没有在外部记录预支出的最大的时间,如果使用原来的workId,那由于不知道预支出到何时,因此无法投入使用。不过这点,我感觉在表中新增一个字段用来存储已经分配的最大时间,那么在等到该时间追平后好像就可以使用了?
而美团的则通过zk注册顺序节点来保证,并且考虑到了在zk节点中存储该节点的最后生成时间,这样可以在节点重启时继续使用而不用分配新的workId。且为了考虑效率,生成时间只在 启动的时候进行校验,内部真实生成id的时候还是比较的内存,而不是从zk现读取。
三、并发优化:多线程安全与性能提升
1. 美团 Leaf:synchronized 关键字保证原子性
Leaf 直接使用synchronized修饰生成 ID 的方法,确保同一时间只有一个线程操作timestamp和sequence:
public synchronized Result get(String key) {
// ID生成逻辑...
}
2. 百度 UidGenerator:PaddedAtomicLong 避免伪共享
UidGenerator 使用自定义的PaddedAtomicLong替代原生AtomicLong,核心解决 CPU 伪共享问题:
public class PaddedAtomicLong extends AtomicLong {
private static final long serialVersionUID = -3415778863941386253L;
// CPU缓存行通常为64字节,通过填充6个long(48字节)避免伪共享
public volatile long p1, p2, p3, p4, p5, p6 = 7L;
}
伪共享问题:多个原子变量若处于同一 CPU 缓存行,一个变量的修改会导致整个缓存行失效,频繁触发缓存同步,降低性能;
优化逻辑:通过填充 6 个 long 变量(共 48 字节),让PaddedAtomicLong的核心值独占一个 64 字节缓存行,避免缓存失效带来的性能损耗。
优势:无锁设计,CAS 操作保证原子性,支持每秒百万级 ID 生成,适合超高并发场景。
四、架构选型:中心化与去中心化的取舍
对于雪花id的组成部分时间戳+workId+序列化三部分而言,其中workId部分如果完成由人工配置,那就是去中心化的,但是如果依赖于zk或者db等方式分配,那么该方案就有一定的中心化因素。且美团的leaf为了监控每个节点的状态,以及节点的上下线,整体依赖zk要更多。不过leaf和UIdGenerator尽管依赖的核心挂掉,那么在已经生成了workId的节点就不会受到影响。
五、网络与部署
由于id本身的生成非常快,那么耗时大部分就是网络耗时。不过这个各个语言都有较成熟的代码了,如果是java,可以直接使用netty在自定义请求中,返回id。如果是c语言,那么可以复用redis或者nginx的网络模型部分,在里面进行处理。
而调用方可以简单的遍历节点列表,加简单的重试机制。或者外包给slb或者nginx,调用方直接调用nginx代理的域名去做处理。
参考: