上周三凌晨2点15分,我们的订单系统突然开始出现异常告警。最初以为是数据库连接池耗尽,但排查后发现更严重的问题——订单表中出现了大量主键冲突。进一步追查发现,系统生成的订单ID竟然出现了重复!这直接导致部分订单无法创建,支付回调处理失败,甚至引发了财务对账差异。
经过长达8小时的紧急排查,最终锁定问题根源:团队自研的分布式ID生成器在长时间运行后开始产生重复ID。这个组件基于雪花算法(Snowflake)改造,已经稳定运行了3年多,却在系统扩容后的第4个月突然暴露出致命缺陷。
Snowflake算法由Twitter提出,其核心思想是将64位Long型整数划分为多个字段:
code复制+----------------------------------------------------------------+
| 1 Bit | 41 Bits Timestamp | 5 Bits DC ID | 5 Bits Worker ID | 12 Bits Sequence |
+----------------------------------------------------------------+
关键设计要点:时间戳占最大比重,确保ID随时间递增;机器标识保证分布式唯一性;序列号解决同一毫秒内的并发问题
41位时间戳可表示的最大值为2^41-1=2199023255551毫秒,约合69.7年。这意味着:
java复制// 时间戳获取示例
long timestamp = System.currentTimeMillis() - EPOCH;
标准实现中,Worker ID通常通过以下方式分配:
我们的"优化版"雪花算法采用了完全不同的位分配方案:
code复制+--------------------------------------------------------------+
| 31 Bits Timestamp | 13 Bits DC ID | 4 Bits Worker | 8 Bits Biz | 8 Bits Seq |
+--------------------------------------------------------------+
问题1:时间循环周期过短
31位时间戳仅能表示2^31毫秒(约24.8天),而我们的epoch设置为2018年。这意味着:
问题2:时间戳位移错误
原始代码中存在位运算错误:
java复制// 错误实现:左移33位但只保留31位时间戳
long id = (timestampDelta << 33) | (dataCenterId << 20) | ...;
Worker ID生成策略:
java复制// 使用IP地址最后一段作为Worker ID
String[] ipSegments = InetAddress.getLocalHost().getHostAddress().split("\\.");
int workerId = Integer.parseInt(ipSegments[3]) % 16;
这种实现存在严重问题:
我们额外添加了8位业务ID字段,导致:
| 方案 | 特点 | 适用场景 | 性能 |
|---|---|---|---|
| UUID | 无序,128位 | 简单场景 | 高 |
| Snowflake | 有序,64位 | 分布式系统 | 极高 |
| Redis INCR | 依赖存储 | 中小规模 | 中 |
| Leaf | 分段缓存 | 高并发 | 高 |
方案1:使用Hutool工具包
java复制// 配置workerId和dataCenterId
Snowflake snowflake = IdUtil.getSnowflake(workerId, dataCenterId);
long id = snowflake.nextId();
方案2:MyBatis-Plus实现
java复制// 自动根据IP生成workerId
DefaultIdentifierGenerator generator = new DefaultIdentifierGenerator();
long id = generator.nextId();
阶段1:开发环境
properties复制# application-dev.properties
snowflake.worker-id=1
snowflake.data-center-id=1
阶段2:容器化部署
java复制// 基于Pod名称生成Worker ID
String podName = System.getenv("POD_NAME");
int workerId = Math.abs(podName.hashCode()) % 32;
阶段3:大规模生产
java复制// 通过Zookeeper分配唯一Worker ID
public int initWorkerId() throws Exception {
CuratorFramework client = CuratorFrameworkFactory.newClient(...);
client.start();
String path = "/snowflake/worker/ids";
InterProcessMutex lock = new InterProcessMutex(client, path + "/lock");
try {
lock.acquire();
List<String> ids = client.getChildren().forPath(path);
// 分配最小可用ID
int allocatedId = findFirstMissing(ids);
client.create().creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(path + "/" + allocatedId);
return allocatedId;
} finally {
lock.release();
}
}
雪花算法对系统时钟敏感,必须处理时钟回拨情况:
java复制public synchronized long nextId() {
long currentMillis = System.currentTimeMillis();
if (currentMillis < lastTimestamp) {
// 时钟回拨处理
long offset = lastTimestamp - currentMillis;
if (offset <= 5) {
// 小幅度回拨,等待
try {
wait(offset << 1);
currentMillis = System.currentTimeMillis();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
// 大幅度回拨,抛出异常
throw new RuntimeException("Clock moved backwards");
}
}
// ...正常生成逻辑
}
java复制// 预计算位移常量
private static final int TIMESTAMP_SHIFT = 22;
private static final int DATA_CENTER_SHIFT = 17;
private static final int WORKER_SHIFT = 12;
public long nextId() {
return (timestamp << TIMESTAMP_SHIFT) |
(dataCenterId << DATA_CENTER_SHIFT) |
(workerId << WORKER_SHIFT) |
sequence;
}
这次事故给我们的核心教训是:在分布式系统中,唯一ID生成看似简单,实则暗藏诸多陷阱。经过这次教训,我们最终迁移到了经过大规模验证的百度UidGenerator方案,并建立了完整的监控体系。对于关键组件,稳定性和可靠性永远应该排在"创新"之前。