JAVA生成订单号(日期+流水号)支持高并发 💥(实战 + 源码)

3,847 阅读3分钟

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 雪花算法

👀 后记

订单号,是架构设计中常被忽略的小细节。
但一个不小心,就可能在高并发下引发重复、冲突、雪崩的问题。

希望这篇文章能帮你写出一个又快又稳的订单系统


如果你觉得这篇文章对你有帮助,欢迎 👉 点赞 + 收藏 + 关注
也欢迎在评论区聊聊你们项目中用的订单号生成方案!