分布式系统中,全局唯一ID生成是分库分表、消息追踪、订单编号等场景的基础需求。数据库自增ID在分库分表后会冲突,UUID无序且占用空间大。雪花算法(Snowflake)通过时间戳+机器ID+序列号的组合,生成趋势递增的64位ID,兼顾有序性和分布式唯一性。百度开源的Leaf系统在雪花算法基础上增加了ZooKeeper协调和Segment号段模式,解决了时钟回拨和机器ID分配问题。本文从原理到部署,给出完整的分布式ID生成方案。
雪花算法原理与结构设计
Snowflake算法生成的ID为64位整数,各bit位含义如下:1bit符号位 + 41bits时间戳 + 10bits机器ID + 12bits序列号。
public class SnowflakeIdGenerator {
private static final long EPOCH = 1704067200000L; // 2024-01-01 UTC
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 must be between 0 and " + MAX_MACHINE_ID);
}
this.machineId = machineId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 时钟回拨检测
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException(
"Clock moved backwards by " +
(lastTimestamp - currentTimestamp) + "ms");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0L) {
currentTimestamp = waitNextMillis(currentTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
时钟回拨问题与解决方案
时钟回拨是雪花算法最棘手的问题。NTP时间同步可能导致系统时钟向后跳转,此时生成的ID可能与之前重复。常见的解决方案包括等待策略和扩展位策略:
// 时钟回拨容忍策略
public synchronized long nextIdWithBackwardTolerance() {
long currentTimestamp = System.currentTimeMillis();
long offset = lastTimestamp - currentTimestamp;
if (offset <= 5 && offset > 0) {
// 回拨5ms以内,等待
try {
Thread.sleep(offset + 1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
currentTimestamp = System.currentTimeMillis();
} else if (offset > 5) {
// 回拨超过容忍范围,使用扩展位标记
return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
| (machineId << MACHINE_ID_SHIFT)
| sequence
| (1L << 63);
}
// 正常生成逻辑...
return 0L;
}
百度Leaf系统架构与部署
百度Leaf提供两种模式:Segment号段模式和Snowflake模式。Segment模式通过数据库批量获取ID号段,减少数据库访问频率;Snowflake模式通过ZooKeeper分配机器ID,解决手工配置问题。
-- Leaf Segment模式数据库表结构
CREATE TABLE leaf_alloc (
biz_tag varchar(128) NOT NULL COMMENT 'business tag',
max_id bigint(20) NOT NULL DEFAULT '1',
step int(11) NOT NULL DEFAULT '1000',
description varchar(256) DEFAULT NULL,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (biz_tag)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO leaf_alloc (biz_tag, max_id, step, description)
VALUES ('order', 1, 2000, 'order ID');
INSERT INTO leaf_alloc (biz_tag, max_id, step, description)
VALUES ('user', 1, 1000, 'user ID');
# Leaf配置文件 leaf.properties
leaf.name=leaf-server
leaf.segment.enable=true
leaf.snowflake.enable=true
leaf.segment.url=jdbc:mysql://127.0.0.1:3306/leaf
leaf.segment.username=root
leaf.segment.password=your_password
leaf.snowflake.zk.address=127.0.0.1:2181
leaf.snowflake.port=2181
Leaf Segment模式的核心逻辑是双Buffer预加载。当前号段使用到10%时,异步加载下一个号段,实现ID获取的无阻塞:
// Leaf Segment双Buffer核心逻辑(简化版)
public class SegmentBuffer {
private volatile Segment segment;
private volatile Segment nextSegment;
private volatile boolean isLoadingNext;
private final String bizTag;
public synchronized long getId() {
if (segment == null) {
segment = loadSegmentFromDB();
}
long id = segment.getAndIncrement();
if (id > segment.getMaxId()) {
if (nextSegment != null) {
segment = nextSegment;
nextSegment = null;
id = segment.getAndIncrement();
} else {
segment = loadSegmentFromDB();
id = segment.getAndIncrement();
}
}
// 使用量超过10%时异步预加载
if (nextSegment == null && !isLoadingNext
&& segment.getUsage() > 0.1) {
isLoadingNext = true;
CompletableFuture.runAsync(() -> {
synchronized (this) {
nextSegment = loadSegmentFromDB();
isLoadingNext = false;
}
});
}
return id;
}
private Segment loadSegmentFromDB() {
// UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = ?
// SELECT max_id, step FROM leaf_alloc WHERE biz_tag = ?
// return [max_id - step + 1, max_id]
return null;
}
}
Leaf Snowflake模式与ZooKeeper协调
Leaf Snowflake模式通过ZooKeeper的持久顺序节点自动分配workerId:
// ZK节点结构
// /leaf/snowflake/
// worker-0000000000 (workerId=0)
// worker-0000000001 (workerId=1)
// worker-0000000002 (workerId=2)
public class SnowflakeZKHolder {
private final String zkPath = "/leaf/snowflake";
private final CuratorFramework zkClient;
private int workerId;
public void init() throws Exception {
// 创建持久顺序节点
String nodePath = zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT_SEQUENTIAL)
.forPath(zkPath + "/worker-");
// 从节点路径解析workerId
String nodeSeq = nodePath.substring(
nodePath.lastIndexOf('-') + 1);
workerId = Integer.parseInt(nodeSeq);
// 写入IP和端口
zkClient.setData().forPath(nodePath,
(NetUtils.getLocalIp() + ":" + port).getBytes());
// 注册临时子节点用于心跳
zkClient.create()
.withMode(CreateMode.EPHEMERAL)
.forPath(nodePath + "/alive");
}
public int getWorkerId() {
return workerId;
}
}
分布式ID方案选型与性能对比
三种方案各有适用场景。原生Snowflake实现简单、性能最高,但需要手动管理机器ID和处理时钟回拨。Leaf Segment模式ID有序、对数据库依赖低,适合对性能要求中等但对稳定性要求高的场景。Leaf Snowflake模式通过ZK自动协调,适合大规模机器部署。
性能参考数据(单节点QPS):原生Snowflake约400万/s,Leaf Segment约100万/s,Leaf Snowflake约300万/s。多节点部署时QPS线性扩展。
选型建议:百台以下机器规模,原生Snowflake配合手动ID分配足够使用;千台规模以上,Leaf Snowflake的ZK协调机制更可靠。对ID连续性有要求的场景(如订单号),Leaf Segment的号段模式生成的ID更紧凑。无论选择哪种方案,时钟回拨检测和workerId唯一性保障都是必须实现的防护措施。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/fen-bu-shi-id-sheng-cheng-fang-an-shi-zhan-xue-hua-suan-fa/