分布式系统中生成全局唯一ID是微服务架构的基础需求。订单号、用户ID、消息流水号等业务场景都要求ID全局唯一、趋势递增、高性能生成。Snowflake雪花算法和美团Leaf是目前主流的两种分布式ID方案,在高并发设计场景下各有适用场景。
Snowflake雪花算法原理与实现
Snowflake由Twitter开源,生成64位Long型ID。结构为:1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号。41位时间戳支持约69年(以毫秒为单位),10位机器ID支持1024个节点,12位序列号单节点每毫秒可生成4096个ID。
public class SnowflakeIdGenerator {
private static final long EPOCH = 1704067200000L;
private static final long MACHINE_ID_BITS = 10L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
private final long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long machineId) {
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
throw new IllegalArgumentException("machineId range: 0-" + MAX_MACHINE_ID);
}
this.machineId = machineId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 时钟回拨检测
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
if (offset <= 5) {
try { Thread.sleep(offset); } catch (InterruptedException e) {}
currentTimestamp = System.currentTimeMillis();
} else {
throw new RuntimeException("Clock moved backwards " + offset + "ms");
}
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
currentTimestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
Snowflake的核心优势是本地生成、无网络依赖、性能极高(单线程每秒百万级)。但存在时钟回拨问题——NTP同步可能导致系统时钟跳变,需要代码层处理。上面的实现采用了小范围等待、大范围抛异常的策略。
时钟回拨问题的多种解决方案
时钟回拨是Snowflake最棘手的问题。除了等待策略,还有几种方案:
方案一:借用未来时间。记录最大时间戳,回拨时使用最大时间戳继续生成,相当于预支了时间。缺点是ID时间戳不精确。
方案二:引入序列号段缓存。每次从Zookeeper/etcd获取机器ID时同时获取一个可用时间偏移量,回拨时使用偏移量补偿。
方案三:使用数据中心+机器双5位划分。将10位机器ID拆分为5位数据中心+5位机器,通过Zookeeper分配唯一标识,降低冲突概率。
美团Leaf发号器架构与部署
Leaf是美团开源的分布式ID生成系统,提供两种模式:Leaf-segment(号段模式)和Leaf-snowflake(雪花模式)。
Leaf-segment通过数据库批量分配号段,每次获取一批ID缓存在本地,减少数据库访问频率。核心表结构:
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) PRIMARY KEY,
max_id BIGINT NOT NULL DEFAULT 1,
step INT NOT NULL DEFAULT 1000,
description VARCHAR(256) DEFAULT NULL,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
// Leaf-segment核心逻辑
public class SegmentIDGenImpl implements IDGen {
private final Map cache = new ConcurrentHashMap<>();
public Result get(final String bizTag) {
SegmentBuffer buffer = cache.get(bizTag);
if (buffer == null) {
buffer = new SegmentBuffer();
cache.putIfAbsent(bizTag, buffer);
buffer = cache.get(bizTag);
}
Segment segment = buffer.getCurrent();
if (!buffer.isInitialized() || segment.getIdle() < segment.getStep() * 0.2) {
updateSegmentFromDb(bizTag, buffer.getNext());
buffer.setNextReady(true);
}
long value = segment.getAtomicId().incrementAndGet();
if (value < segment.getMaxId()) {
return new Result(value, Status.SUCCESS);
}
synchronized (buffer) {
if (buffer.isInitialized() && buffer.getNextReady()) {
buffer.switchSegment();
buffer.setNextReady(false);
return get(bizTag);
}
}
return new Result(0, Status.EXCEPTION);
}
}
双Buffer策略是Leaf-segment的关键设计。当前号段使用到80%时,异步加载下一个号段到备用Buffer,号段切换时无阻塞。这解决了单Buffer方案在号段耗尽时需要同步等待数据库的问题。
Snowflake与Leaf方案对比与选型
| 维度 | Snowflake | Leaf-segment | Leaf-snowflake |
|---|---|---|---|
| 依赖 | 无 | MySQL | Zookeeper |
| 性能 | 百万QPS | 十万QPS | 百万QPS |
| ID特征 | 趋势递增 | 严格递增 | 趋势递增 |
| 时钟回拨 | 需自行处理 | 无此问题 | 框架处理 |
| 运维复杂度 | 低 | 中 | 中 |
| 扩展性 | 机器ID管理 | biz_tag扩展 | ZK管理 |
选型建议:单机场景用Snowflake足够,无需额外依赖;对ID严格递增有要求的业务(如数据库分库分表按ID范围查询)选Leaf-segment;多机房部署选Leaf-snowflake,Zookeeper管理机器ID避免冲突。
生产环境部署注意事项
Snowflake方案部署时,机器ID分配需要保证全局唯一。推荐通过启动参数传入或配置中心管理。以下是基于Zookeeper的机器ID自动分配:
public class ZkMachineIdAllocator {
private final String zkPath = "/snowflake/machine-ids";
private final CuratorFramework zkClient;
public long allocateMachineId() {
String nodePath = zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(zkPath + "/node-");
String nodeNum = nodePath.substring(nodePath.lastIndexOf("-") + 1);
long machineId = Long.parseLong(nodeNum) % 1024;
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { zkClient.delete().forPath(nodePath); }
catch (Exception e) { }
}));
return machineId;
}
}
临时顺序节点在服务重启时自动释放旧ID,避免机器ID泄漏。取模1024保证ID在合法范围内。生产环境建议配合监控告警,当可用机器ID低于100时触发扩容评估。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/fen-bu-shi-id-sheng-cheng-shi-zhan-snowflake-xue-hua-suan/