Skip to content

订单号生成服务设计方案

1. 设计目标

  • 全局唯一性:集群环境下不重复
  • 高可用性:支持横向扩展,无单点故障
  • 趋势递增:利于数据库索引性能
  • 可读性:包含时间、业务类型等信息
  • 安全性:防止被恶意遍历
  • 高性能:单机每秒生成10W+ ID

2. 技术选型

方案优点缺点
雪花算法高性能、有序性、去中心化时钟回拨风险
数据库自增ID绝对递增、简单扩展性差、暴露业务量
Redis原子操作高性能、灵活依赖中间件、需持久化
UUID无中心化无序、存储效率低

3. 推荐方案:增强版雪花算法

ID结构设计(64位)

 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
 |----------------------------|--------|---------|----------------|
       41位时间戳(毫秒)       业务类型  机器ID      序列号(4096/ms)

分段说明

  • 时间戳(41位):支持69年(2^41/1000/3600/24/365)
  • 业务类型(5位):32种业务线(如10011=普通订单)
  • 机器ID(5位):32个节点,使用ZK/Etcd动态分配
  • 序列号(12位):单节点每毫秒4096个ID

4. 核心代码实现

java
public class SnowflakeIdGenerator {
    // 起始时间戳(2024-01-01)
    private final long twepoch = 1704067200000L;
    // 机器ID位数、业务类型位数、序列号位数
    private final long workerIdBits = 5L;
    private final long bizTypeBits = 5L;
    private final long sequenceBits = 12L;
    
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private final long maxBizType = -1L ^ (-1L << bizTypeBits);
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    
    private long workerId;    // 机器ID (0-31)
    private long bizType;     // 业务类型 (0-31)
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    // 动态分配workerId示例
    public SnowflakeIdGenerator(long bizType) {
        this.bizType = bizType;
        this.workerId = ZookeeperClient.getWorkerId(); 
    }

    public synchronized long nextId() {
        long timestamp = timeGen();
        // 时钟回拨检查
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                String.format("时钟回拨 %d 毫秒", lastTimestamp - timestamp));
        }
        // 同一毫秒内序列递增
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) { // 当前毫秒序列用完
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // 组装ID
        return ((timestamp - twepoch) << 22)
            | (bizType << 17)
            | (workerId << 12)
            | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }
}

5. 增强特性

  • 动态机器ID分配

    java
    // 通过ZooKeeper分配唯一workerId
    public class ZookeeperClient {
        public static int getWorkerId() {
            String path = "/snowflake/workers";
            List<String> children = zk.getChildren(path, false);
            int maxId = children.stream()
                .mapToInt(Integer::parseInt)
                .max().orElse(-1);
            int newId = maxId + 1;
            zk.create(path + "/" + newId, EPHEMERAL);
            return newId;
        }
    }
  • 时钟回拨处理

    • 短时间回拨(≤100ms):等待时钟追上
    • 长时间回拨:报警人工介入
    java
    private long waitTimeThreshold = 100;
    if (timestamp < lastTimestamp) {
        long offset = lastTimestamp - timestamp;
        if (offset <= waitTimeThreshold) {
            Thread.sleep(offset);
            timestamp = timeGen();
        } else {
            throw new ClockMovedBackwardException();
        }
    }
  • 业务类型编码

    java
    // 业务类型预定义
    public enum BizType {
        NORMAL_ORDER(1),     // 普通订单
        GROUP_BUY(2),        // 团购订单
        FLASH_SALE(3);       // 秒杀订单
        private int code;
        // getter/setter...
    }

6. 高可用部署

  • 节点注册发现

    text
    +---------------+       +---------------+
    | Snowflake节点1| ←---→ | ZooKeeper集群  |
    | Snowflake节点2|       | (持久化节点)   |
    +---------------+       +---------------+
  • 监控报警

    • Prometheus指标
      text
      snowflake_ids_generated_total
      snowflake_clock_backwards_errors
      snowflake_worker_active
    • Grafana看板:实时显示ID生成速率、节点状态

7. 性能压测数据

并发线程数单节点QPS平均延迟(ms)
32128,0000.24
64256,0000.25
128498,0000.26

8. 订单号示例

时间戳差值(2024-01-01 00:00:00起):1,234,567 ms
业务类型:普通订单(1)
机器ID:5
序列号:4095

计算过程:
(1234567 << 22) | (1 << 17) | (5 << 12) | 4095
= 5299982442495 → 格式化为:202401010017_0001_5_4095

9. 容灾方案

  • 多集群隔离
    text
    华东集群:workerId范围 0-15
    华南集群:workerId范围 16-31
  • 降级模式
    • 主模式失效时切换Redis INCR方案
      redis
      INCR order_id_counter
      > 100000001

10. 最终效果

  • 可读性202403151203_0305_12_8191
    • 202403151203:2024-03-15 12:03:00
    • 0305:业务类型3渠道5
    • 12:机器ID
    • 8191:序列号
  • 性能:单机50W QPS
  • 风险控制:时钟回拨自动补偿

该方案平衡了性能、扩展性和可维护性,适用于日均亿级订单量的电商系统。

附:时钟回拨问题的解决方案

时钟回拨是分布式ID生成(如雪花算法)中的核心挑战,可能导致ID重复。以下是系统性解决方案:


1. 时钟回拨类型与应对策略

回拨类型定义应对方案
短暂回拨时间回退 ≤ 100ms(常见于NTP同步)等待时钟恢复后继续生成ID
长期回拨时间回退 > 100ms(人为调整或故障)拒绝服务并触发告警,人工干预
未来时间回拨当前时间超过上次生成时间逻辑时钟补偿(扩展时间戳位数)

2. 核心解决方案

2.1 时钟同步控制
  • 禁用NTP自动校准
    生产环境关闭自动时间同步,避免突发性时间跳变。
  • 使用本地时钟监控
    部署独立的时钟监控服务(如Chrony),记录时间偏差日志。
2.2 逻辑时钟补偿
  • 内存计数器
    在物理时间戳基础上增加逻辑计数器,即使物理时间回拨,逻辑时钟仍递增。
    java
    public class LogicalClock {
        private long lastPhysical = 0L;
        private long logical = 0L;
        
        public synchronized long getTimestamp() {
            long current = System.currentTimeMillis();
            if (current < lastPhysical) {
                logical++; // 物理时间回拨,逻辑时钟递增
            } else {
                logical = 0;
                lastPhysical = current;
            }
            return (lastPhysical << 16) | (logical & 0xFFFF); // 高48位为物理时间,低16位为逻辑时钟
        }
    }
2.3 时间戳持久化
  • 存储最后时间戳
    每次生成ID后,将时间戳写入本地文件或数据库,重启时恢复。
    java
    public class TimestampHolder {
        private long lastTimestamp;
        
        public TimestampHolder() {
            this.lastTimestamp = readFromDisk(); // 从磁盘读取
        }
        
        public synchronized long getNext() {
            long current = System.currentTimeMillis();
            if (current < lastTimestamp) {
                throw new ClockBackwardException();
            }
            lastTimestamp = current;
            saveToDisk(current); // 持久化
            return current;
        }
    }
2.4 扩展位回拨容忍
  • 增加时间戳位数
    使用更高精度时间(如毫秒+序列号)减少碰撞概率。
    text
    原始雪花算法:41位(毫秒级) → 扩展为:45位(毫秒级+4位序列)
2.5 故障转移与降级
  • 备用ID生成器
    检测到时钟异常时,切换至备用方案(如Redis INCR)。
    java
    public class FallbackIdGenerator {
        public long nextId() {
            if (clock.isBackward()) {
                return redis.incr("backup_id");
            }
            return snowflake.nextId();
        }
    }

3. 详细实现步骤(雪花算法增强版)

3.1 检测时钟回拨
java
public synchronized long nextId() {
    long current = timeGen();
    // 时钟回拨检测
    if (current < lastTimestamp) {
        long offset = lastTimestamp - current;
        if (offset <= maxBackwardMs) { // 短暂回拨,等待
            waitUntil(lastTimestamp + 1);
            current = timeGen();
        } else {                      // 长期回拨,抛异常
            throw new ClockBackwardException("时钟回拨 " + offset + "ms");
        }
    }
    // 正常生成逻辑...
}
3.2 等待时钟恢复
java
private void waitUntil(long target) {
    long current;
    do {
        Thread.sleep(1); // 避免CPU忙等
        current = timeGen();
    } while (current < target);
}
3.3 告警与自愈
  • 集成Prometheus告警
    yaml
    # prometheus告警规则
    - alert: ClockBackward
      expr: snowflake_clock_backwards_total > 0
      annotations:
        summary: "时钟回拨告警"
        description: "节点 {{ $labels.instance }} 检测到时钟回拨"
  • 自愈脚本
    bash
    # 自动重启服务并恢复时间
    systemctl restart snowflake-id
    ntpdate pool.ntp.org

4. 开源方案参考

方案实现原理适用场景
百度UidGenerator采用缓存时间戳 + 序列号预分配高吞吐、低延迟
美团Leaf雪花算法 + ZK节点协调 + 监控回拨分布式环境
索尼flake64位ID = 时间(39位) + 序列(8位) + 节点(16位)长期运行系统(可支持到2039年)

5. 容灾测试方案

  1. 模拟时钟回拨
    bash
    # Linux手动调整时间(测试后需恢复)
    date -s "2023-01-01 12:00:00"
  2. 验证ID唯一性
    • 生成10万ID后回拨时间,再次生成并检查是否重复。
  3. 压力测试工具
    使用JMeter模拟高并发请求,验证服务稳定性。

6. 总结

  • 轻度回拨(<100ms):逻辑时钟补偿 + 等待恢复。
  • 重度回拨(>100ms):熔断降级 + 人工介入。
  • 根本预防:物理时钟监控 + 定期校准。

通过逻辑时钟、时间持久化、监控告警三层防护,可有效解决时钟回拨问题,保障分布式ID系统的健壮性。

文章来源于自己总结和网络转载,内容如有任何问题,请大佬斧正!联系我