我是怎么设计一个订单号生成策略的(库存系统)
一、背景
最近我在做一套自研的库存管理系统,其中有一个看似简单、实则很关键的功能:订单号生成策略。
订单号不仅要全局唯一,还要有一定的可读性和业务含义,比如能一眼看出是入库单还是出库单、是哪个用户、什么时间生成的。这样在后续查日志、对账、排查问题的时候才更方便。
市面上虽然有很多现成的 ID 生成方案,比如 UUID、Snowflake、Leaf 等等,但它们都有一个共同的问题:没有业务语义。
比如这个订单号到底是入库单还是出库单?用户是谁?哪天生成的?看不出来。
所以,我决定自己设计一个订单号生成策略,结合 Redis、时间戳、业务标识和用户信息,实现一个既唯一、又可读、还易维护的方案。
二、我遇到的问题
在设计过程中,我遇到了几个关键问题:
- 如何保证订单号全局唯一?
- 怎么让订单号有业务含义?
- 如何支持分布式部署?
- Redis 生成自增ID会不会成为瓶颈?
- 如何避免 Redis Key 堆积?
带着这些问题,我开始一步步设计我的订单号生成逻辑。
三、我是怎么做的?
我最终设计了一个订单号结构如下:
[业务码][机器码][用户码][日期][自增ID]
每个字段的含义如下:
字段 | 长度 | 示例 | 说明 |
---|---|---|---|
业务码 | 2位 | RK、CK | 入库(RK)、出库(CK) |
机器码 | 2位 | 01、02 | 表示部署节点 |
用户码 | 6位 | 000123 | 用户ID后6位 |
日期 | 8位 | 20250719 | 格式为YYYYMMDD |
自增ID | 6位 | 000001 | 每天从1开始递增 |
示例订单号:
RK0100012320250719000001
- RK:入库订单
- 01:机器编号
- 000123:用户ID后6位
- 20250719:订单生成日期
- 000001:当天该用户该业务类型的第一个订单
四、技术实现细节
1. 使用 Redis 生成自增ID
我用 Redis 的 INCR
命令来生成每天的自增ID,Key 的格式如下:
order:${businessType}:${date}:${userCode}
例如:
INCR order:RK:20250719:000123
这样可以保证:
- 同一用户、同一天、同一业务类型的订单号唯一
- 每天自动重置计数,避免ID无限增长
2. Java 实现代码
public class OrderNoGenerator {private RedisTemplate<String, String> redisTemplate;public OrderNoGenerator(RedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;}public String generateOrderNo(String businessType, int machineId, long userId) {// 1. 业务类型(2位)String bizCode = businessType;// 2. 机器ID(2位)String machineCode = String.format("%02d", machineId);// 3. 用户ID(6位)String userCode = String.format("%06d", userId % 1000000); // 截取后6位// 4. 当前日期(8位)String dateCode = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);// 5. Redis 自增ID(6位)String redisKey = String.format("order:%s:%s:%s", bizCode, dateCode, userCode);Long incrId = redisTemplate.opsForValue().increment(redisKey);String incrCode = String.format("%06d", incrId);// 6. 组装订单号return bizCode + machineCode + userCode + dateCode + incrCode;}
}
3. Redis Key 管理与清理
为了避免 Redis Key 无限增长,我加了一个定时任务,每天凌晨清理前一天的 Key:
@Scheduled(cron = "0 0 0 * * ?")
public void clearYesterdayOrderKeys() {String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.BASIC_ISO_DATE);Set<String> keys = redisTemplate.keys("order:*:" + yesterday + ":*");if (keys != null && !keys.isEmpty()) {redisTemplate.delete(keys);}
}
五、难点与优化点
难点:
-
如何保证订单号唯一性?
答案是:Redis + 业务类型 + 时间 + 用户ID组合,保障唯一。 -
如何避免 Redis 成为瓶颈?
Redis 的INCR
是原子操作,性能很好,但为了进一步优化,也可以采用“分段缓存”机制,比如一次取100个ID本地缓存使用。 -
如何让订单号具备可读性?
通过字段拼接,让订单号包含业务类型、用户、时间等信息,方便日志追踪。
优化建议:
- 分段自增机制:减少 Redis 调用频率
- 用户ID压缩算法:如 CRC32 或 MurmurHash,生成更短的用户码
- 日志与监控:记录生成的订单号,异常时自动报警
- 多机部署支持:通过机器码区分不同节点,避免冲突
六、实际效果如何?
这套订单号生成策略在我们系统中上线后,运行稳定,效果不错:
- 订单号唯一性得到保障,未出现重复
- 日志追踪、对账、排查问题都变得容易
- Redis 性能良好,未出现瓶颈
- 支持多节点部署,适配分布式环境
七、总结
通过结合 Redis 自增ID、业务标识、时间戳和用户信息,我实现了一个适合库存系统的订单号生成策略,具有以下优势:
优势 | 说明 |
---|---|
唯一性强 | Redis + 时间 + 用户 + 业务组合保障 |
可读性高 | 能看出订单类型、用户、时间等信息 |
结构清晰 | 易于日志追踪和调试 |
分布式支持 | 机器码支持多节点部署 |
易于维护 | 支持定时清理、日志记录、异常监控 |
如果你也在开发类似的库存系统、订单系统或支付系统,不妨参考这套方案。希望这篇文章能对你有所帮助!