一、需求分析

  1. 唯一性:全局唯一,绝不重复。

  2. 高可用性:支持高并发生成(如每秒数万订单)。

  3. 可扩展性:适应业务增长,支持分布式部署。

  4. 可读性(可选):包含时间、业务类型等信息。

  5. 防猜测性:避免通过订单号推断业务规模或遍历数据。

  6. 兼容性:支持分库分表、业务扩展(如不同业务线标识)。


二、技术方案选型

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. 时间戳
  • 精确到秒或毫秒(毫秒级需扩展位数)。

  • 解决时钟回拨

    • 记录最后一次生成时间戳,若检测到回拨,则:

    1. 回拨时间短(<100ms):等待时钟追平。

    2. 回拨时间长:报警并拒绝生成,或切换到备用节点。

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;


四、高可用与容灾

  1. 多节点部署

    • 部署多个订单号生成服务,通过负载均衡分发请求。

    • 每个节点配置唯一Worker ID(通过配置中心动态分配)。

  2. 降级策略

    • 主生成服务故障时,切换到备用算法(如UUID或数据库自增)。

  3. 监控与报警

    • 监控时钟同步状态、Worker ID分配、序列号耗尽等情况。


五、性能优化

  1. 本地缓存预生成

    • 提前生成一批ID缓存在内存,减少实时计算压力。

  2. 无锁设计

    • 使用ThreadLocalRandom替代同步块,或CAS(Compare-And-Swap)更新序列号。

  3. 二进制操作优化

    • 位运算替代字符串拼接,提升性能。


六、示例代码(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;
    }
}


七、测试验证

  1. 唯一性测试

    • 启动多线程(如1000线程)并发生成10万次,检查是否重复。

  2. 性能压测

    • 使用JMeter模拟每秒10万请求,观察生成耗时和系统负载。

  3. 时钟回拨测试

    • 修改系统时间,验证异常处理逻辑。


八、扩展性考虑

  1. 业务编码扩展:预留字段支持新业务类型。

  2. ID长度扩展:未来可增加时间戳精度或机器ID位数。

  3. 多数据中心:在订单号中加入数据中心标识(如前2位表示地区)。