生成唯一订单号的高并发方案对比

93 阅读2分钟

图片

01   背景:订单号到底难在哪?

在电商、支付、物流系统里,订单号=业务生命线,它必须:

  •  全球唯一
  •  时间递增(方便分库分表、索引优化)
  •  可读性高(一眼看出业务+日期)
  •  绝不依赖数据库自增(高并发下锁竞争是灾难)

所以,最常用也最稳妥的做法就是:时间戳 + 当日流水号

20250818 + 000001 → 20250818000001

02  传统方案误区

方案问题
数据库自增主键每次都要写库,高并发直接锁表 → Pass
SimpleDateFormat + 静态计数器线程不安全,重复订单号 → Pass

03  终极方案:本地内存原子计数器

直接上线程安全、零依赖、单机百万级QPS的代码!

源码 OrderNoGenerator.java

package com.example.order;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class OrderNoGenerator {

    /** 每天最大订单数 99万 */
    private static final int MAX_PER_DAY = 999_999;

    /** 按日期隔离的原子计数器 */
    private static final ConcurrentHashMap<String, AtomicInteger> DATE_COUNTER =
            new ConcurrentHashMap<>();

    /** 生成订单号 */
    public static String next() {
        // 1. 拿到今天的 yyyyMMdd
        String today = LocalDate.now()
                                .format(DateTimeFormatter.BASIC_ISO_DATE); // 20250818

        // 2. 原子计数器:不存在就创建,存在就复用
        AtomicInteger counter = DATE_COUNTER
                .computeIfAbsent(today, k -> new AtomicInteger(0));

        // 3. 原子+1,拿到流水号
        int serial = counter.incrementAndGet();
        if (serial > MAX_PER_DAY) {
            throw new IllegalStateException("今日订单量爆表!");
        }

        // 4. 补零到6位并拼接
        return today + String.format("%06d", serial);
    }

    /** 定时清理旧缓存,防止内存泄露 */
    public static void gcOld() {
        String today = LocalDate.now()
                                .format(DateTimeFormatter.BASIC_ISO_DATE);
        DATE_COUNTER.keySet().removeIf(date -> !date.equals(today));
    }
}

注解:
1、 ConcurrentHashMap + AtomicInteger:CAS无锁,线程安全且性能炸裂。

2、 computeIfAbsent:只在第一次访问时创建,避免重复 new。

3、 String.format("%06d", serial) :固定6位,左补零,易读易排序。

04 分布式场景:Redis 原子自增

单机方案再好,也扛不住集群。

分布式就用 Redis INCR

Redis 写法

// 伪代码,SpringBoot 环境
String key = "order:serial:" + LocalDate.now()
                                       .format(DateTimeFormatter.BASIC_ISO_DATE);
Long serial = redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, Duration.ofDays(1));
String orderNo = key.substring(key.length() - 8) + String.format("%06d", serial);

压测结果:单机 10 线程 100 万次调用

指标结果
重复订单号0 
平均耗时< 1 μs 
最大TPS≈ 120 万/秒

小结

场景推荐方案
单机本地 AtomicInteger
分布式Redis INCR
需要业务前缀 / 分库分表Snowflake 雪花算法

参考:

从Atomic到Redis:Java生成唯一订单号的3种高并发方案对比