一、需求分析
唯一性:全局唯一,绝不重复。
高可用性:支持高并发生成(如每秒数万订单)。
可扩展性:适应业务增长,支持分布式部署。
可读性(可选):包含时间、业务类型等信息。
防猜测性:避免通过订单号推断业务规模或遍历数据。
兼容性:支持分库分表、业务扩展(如不同业务线标识)。
二、技术方案选型
1. 常见订单号生成方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
数据库自增ID | 简单、严格递增 | 单点瓶颈、暴露业务量 | 小规模单机系统 |
UUID | 唯一性强、无中心化依赖 | 无序、可读性差、存储空间大 | 简单分布式系统 |
Snowflake算法 | 高性能、趋势递增、可读时间戳 | 依赖时钟同步、需解决时间回拨 | 高并发分布式系统 |
分段发号(号段模式) | 高性能、数据库压力小 | 需预分配号段、可能浪费ID | 高并发且允许少量浪费 |
Redis自增 | 简单、性能较好 | Redis单点风险、需持久化 | 中等规模分布式系统 |
2. 推荐方案:改进型Snowflake算法
综合高并发、可扩展性和可读性,推荐使用增强版Snowflake算法,结合业务编码和时间戳。
三、详细设计
1. 订单号格式设计
示例订单号:20231109141930123456789A1B2C
组成结构(可根据业务调整):
时间戳(14位):
yyyyMMddHHmmss
(如20231109141930)业务标识(2位):区分业务线(如01=普通订单,02=秒杀订单)
机器ID(3位):分布式节点唯一标识
随机序列(8位):时间戳内的递增序列 + 随机数(防猜测)
校验位(1位):防止输入错误(如Luhn算法)
分表结果:有可能会存
2. 关键组件实现
a. 时间戳
精确到秒或毫秒(毫秒级需扩展位数)。
解决时钟回拨:
记录最后一次生成时间戳,若检测到回拨,则:
回拨时间短(<100ms):等待时钟追平。
回拨时间长:报警并拒绝生成,或切换到备用节点。
b. 机器ID(Worker ID)
分配方式:
静态配置:适用于固定服务器规模(需人工管理)。
动态注册:使用ZooKeeper/Etcd/DB分配唯一ID,支持自动扩缩容。
推荐实现:
// 通过数据库获取或注册Worker ID public class WorkerIdManager { private static int workerId; public static synchronized int initWorkerId() { // 从数据库或配置中心获取唯一ID workerId = fetchWorkerIdFromDB(); return workerId; } }
c. 序列号
每个时间单位(如秒)内自增,支持高并发:
public 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做轻量加密(如异或操作)。
示例:
// 在序列号后追加随机数 long baseId = snowflakeNextId(); String orderId = baseId + ThreadLocalRandom.current().nextInt(1000);
e. 校验位(可选)
使用Luhn算法或简单取模:
public 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位作为分片路由(需提前规划分片数量)。
示例:
// 根据用户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)
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位表示地区)。