一、需求分析
- 唯一性:全局唯一,绝不重复。
- 高可用性:支持高并发生成(如每秒数万订单)。
- 可扩展性:适应业务增长,支持分布式部署。
- 可读性(可选):包含时间、业务类型等信息。
- 防猜测性:避免通过订单号推断业务规模或遍历数据。
- 兼容性:支持分库分表、业务扩展(如不同业务线标识)。
二、技术方案选型
1. 常见订单号生成方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
数据库自增ID | 简单、严格递增 | 单点瓶颈、暴露业务量 | 小规模单机系统 |
UUID | 唯一性强、无中心化依赖 | 无序、可读性差、存储空间大 | 简单分布式系统 |
Snowflake算法 | 高性能、趋势递增、可读时间戳 | 依赖时钟同步、需解决时间回拨 | 高并发分布式系统 |
分段发号(号段模式) | 高性能、数据库压力小 | 需预分配号段、可能浪费ID | 高并发且允许少量浪费 |
Redis自增 | 简单、性能较好 | Redis单点风险、需持久化 | 中等规模分布式系统 |
2. 推荐方案:改进型Snowflake算法
综合高并发、可扩展性和可读性,推荐使用增强版Snowflake算法,结合业务编码和时间戳。
三、详细设计
1. 订单号格式设计
plaintext
示例订单号:20231109141930123456789A1B2C
- 组成结构 (可根据业务调整):
- 时间戳 (14位):
yyyyMMddHHmmss
(如20231109141930) - 业务标识(2位):区分业务线(如01=普通订单,02=秒杀订单)
- 机器ID(3位):分布式节点唯一标识
- 随机序列(8位):时间戳内的递增序列 + 随机数(防猜测)
- 校验位(1位):防止输入错误(如Luhn算法)
- 分表结果:有可能会存
- 时间戳 (14位):
2. 关键组件实现
a. 时间戳
- 精确到秒或毫秒(毫秒级需扩展位数)。
- 解决时钟回拨 :
- 记录最后一次生成时间戳,若检测到回拨,则:
- 回拨时间短(<100ms):等待时钟追平。
- 回拨时间长:报警并拒绝生成,或切换到备用节点。
- 记录最后一次生成时间戳,若检测到回拨,则:
b. 机器ID(Worker ID)
-
分配方式 :
- 静态配置:适用于固定服务器规模(需人工管理)。
- 动态注册:使用ZooKeeper/Etcd/DB分配唯一ID,支持自动扩缩容。
-
推荐实现 :
java// 通过数据库获取或注册Worker ID public class WorkerIdManager { private static int workerId; public static synchronized int initWorkerId() { // 从数据库或配置中心获取唯一ID workerId = fetchWorkerIdFromDB(); return workerId; } }
c. 序列号
-
每个时间单位(如秒)内自增,支持高并发:
javapublic class SequenceGenerator { private long lastTimestamp = -1L; private long sequence = 0L; public synchronized long nextId() { long timestamp = System.currentTimeMillis(); if (timestamp < lastTimestamp) { throw new ClockMovedBackException(); } if (timestamp == lastTimestamp) { sequence = (sequence + 1) & MAX_SEQUENCE; if (sequence == 0) { // 当前毫秒序列用完,等待下一毫秒 timestamp = waitNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = timestamp; return ((timestamp << TIMESTAMP_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence); } }
d. 随机化与防猜测
-
混合随机数:在序列号中插入随机位。
-
加密混淆:对生成的ID做轻量加密(如异或操作)。
-
示例 :
java// 在序列号后追加随机数 long baseId = snowflakeNextId(); String orderId = baseId + ThreadLocalRandom.current().nextInt(1000);
e. 校验位(可选)
-
使用Luhn算法或简单取模:
javapublic static char generateCheckDigit(String orderId) { int sum = 0; for (int i = 0; i < orderId.length(); i++) { int digit = Character.getNumericValue(orderId.charAt(i)); sum += (i % 2 == 0) ? digit * 2 : digit; } return (10 - (sum % 10)) % 10; }
3. 分库分表支持
-
方案1:订单号中嵌入分片键(如用户ID哈希值)。
-
方案2:使用订单号的最后N位作为分片路由(需提前规划分片数量)。
-
示例 :
java// 根据用户ID计算分片 int shard = userId.hashCode() % SHARD_NUM; String orderId = time + businessCode + machineId + sequence + shard;
四、高可用与容灾
- 多节点部署 :
- 部署多个订单号生成服务,通过负载均衡分发请求。
- 每个节点配置唯一
Worker ID
(通过配置中心动态分配)。
- 降级策略 :
- 主生成服务故障时,切换到备用算法(如UUID或数据库自增)。
- 监控与报警 :
- 监控时钟同步状态、Worker ID分配、序列号耗尽等情况。
五、性能优化
- 本地缓存预生成 :
- 提前生成一批ID缓存在内存,减少实时计算压力。
- 无锁设计 :
- 使用
ThreadLocalRandom
替代同步块,或CAS(Compare-And-Swap)更新序列号。
- 使用
- 二进制操作优化 :
- 位运算替代字符串拼接,提升性能。
六、示例代码(Java)
java
public class OrderIdGenerator {
private final long workerId;
private long lastTimestamp = -1L;
private long sequence = 0L;
private static final int SEQUENCE_BITS = 12;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
public OrderIdGenerator(long workerId) {
this.workerId = workerId;
}
public synchronized String generate() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
long id = ((timestamp << 22)
| (workerId << 10)
| sequence);
// 添加业务编码和校验位
return String.format("%016X%02d%01d", id, businessCode, checkDigit(id));
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
七、测试验证
- 唯一性测试 :
- 启动多线程(如1000线程)并发生成10万次,检查是否重复。
- 性能压测 :
- 使用JMeter模拟每秒10万请求,观察生成耗时和系统负载。
- 时钟回拨测试 :
- 修改系统时间,验证异常处理逻辑。
八、扩展性考虑
- 业务编码扩展:预留字段支持新业务类型。
- ID长度扩展:未来可增加时间戳精度或机器ID位数。
- 多数据中心:在订单号中加入数据中心标识(如前2位表示地区)。