JAVA生成订单号(日期+流水号)支持高并发 💥(实战 + 源码)
📌 作者:天天摸鱼的java工程师(8年后端经验)
🚀 擅长高并发、分布式、系统架构
📆 今日话题:如何优雅且高性能地生成订单号
💡 背景:为什么要自定义订单号?
在电商、支付、物流等系统中,订单号是核心字段之一:
- 要求全球唯一
- 要求按时间有序(方便分库分表、查询优化)
- 有时还需要可读性高(如业务前缀、时间戳)
- 不能依赖数据库自增主键(高并发下是性能瓶颈)
所以,我们一般会选择 “时间戳 + 流水号” 的方式来自定义订单号。
🧠 目标设计
✅ 订单号格式示例:
20250818 + 000001 → 20250818000001
- 前缀:当前日期(如
20250818) - 后缀:每日从1递增的流水号,长度固定(如
000001)
✅ 设计要求:
- 支持高并发生成
- 同一日期下,流水号全局唯一
- 跨天自动重置计数
- 不依赖数据库写入(减少锁竞争)
- 保证线程安全
❌ 常见错误实现(别踩坑)
❌ 1. 使用数据库自增字段
INSERT INTO orders (xxx) VALUES (xxx);
SELECT 4189843;
- 慢!每次都要写数据库
- 高并发下是瓶颈 → 不推荐
❌ 2. 使用 SimpleDateFormat + static 变量
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String orderNo = sdf.format(new Date()) + (++counter);
SimpleDateFormat非线程安全- 静态变量
counter无同步,线程不安全 - 会出现重复订单号
✅ 正确实现方式(推荐)
🚀 技术方案:使用 ConcurrentHashMap + AtomicInteger + LocalDate
public class OrderNoGenerator {
// 保存每天的计数器
private static final ConcurrentHashMap<String, AtomicInteger> dateCounterMap = new ConcurrentHashMap<>();
// 每日最大单量(根据业务调整)
private static final int MAX_PER_DAY = 999999;
/**
* 生成订单号:格式为 yyyyMMdd + 六位流水号
*/
public static String generateOrderNo() {
// 获取当前日期字符串
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
// 获取当天的计数器
AtomicInteger counter = dateCounterMap.computeIfAbsent(date, key -> new AtomicInteger(0));
int serial = counter.incrementAndGet(); // 原子递增
if (serial > MAX_PER_DAY) {
throw new RuntimeException("当天订单数量超过上限!");
}
// 格式化为六位流水号
String serialStr = String.format("%06d", serial);
return date + serialStr;
}
}
🧪 测试验证(并发测试)
public class TestOrderNo {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(10);
Set<String> orderSet = ConcurrentHashMap.newKeySet();
CountDownLatch latch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
pool.execute(() -> {
String orderNo = OrderNoGenerator.generateOrderNo();
if (!orderSet.add(orderNo)) {
System.err.println("重复订单号:" + orderNo);
}
latch.countDown();
});
}
latch.await();
pool.shutdown();
System.out.println("生成订单数:" + orderSet.size());
}
}
✅ 输出结果:
生成订单数:1000
✅ 所有订单号唯一,线程安全无误
🧰 优化建议
🔁 自动清理过期日期的计数器(防内存泄漏)
// 使用 ScheduledExecutorService 定时清理
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
dateCounterMap.keySet().removeIf(date -> !date.equals(today));
}, 1, 1, TimeUnit.HOURS);
🧱 衍生设计:支持分布式系统?
在分布式系统中,可以考虑:
✅ 1. Redis 原子自增(推荐)
String key = "order_no:" + LocalDate.now();
Long serial = redisTemplate.opsForValue().increment(key, 1);
redisTemplate.expire(key, 1, TimeUnit.DAYS);
- 保证全局唯一
- Redis 本身支持高并发
- 可扩展性强
✔️ 总结
| 特性 | 本地实现 | Redis实现 |
|---|---|---|
| 并发性能 | 高(适合单机) | 更高(分布式支持) |
| 内存占用 | 需要手动清理历史数据 | Redis自动过期 |
| 可扩展性 | 有限 | 可无限扩展 |
| 复杂度 | 简单 | 需配置Redis |
📌 最佳实践建议
- 单机项目:本地 AtomicInteger 方案
- 分布式环境:Redis Atomic 方案
- 需要更复杂规则(如业务前缀、分库分表):推荐Snowflake 雪花算法
👀 后记
订单号,是架构设计中常被忽略的小细节。
但一个不小心,就可能在高并发下引发重复、冲突、雪崩的问题。
希望这篇文章能帮你写出一个又快又稳的订单系统!
如果你觉得这篇文章对你有帮助,欢迎 👉 点赞 + 收藏 + 关注,
也欢迎在评论区聊聊你们项目中用的订单号生成方案!