Skip to content

04 - 分布式ID生成

知识图谱

                    RedisIdWorker.nextId("order")

                ┌───────────┴───────────┐
                │                       │
          时间戳部分(32bit)          序列号部分(32bit)
                │                       │
    nowSecond - BEGIN_TIMESTAMP    Redis INCR 自增
    (1640995200 = 2022-01-01)     key = icr:{prefix}:{yyyy:MM:dd}
                │                       │
                └───────────┬───────────┘

                    timestamp << 32 | count

                     64位 Long 型 ID

              ┌─────────────┴──────────────┐
              │  高32位: 时间戳(支持~136年)  │
              │  低32位: 序列号(每天43亿)    │
              └────────────────────────────┘

核心代码走读

文件: src/main/java/com/hmdp/utils/RedisIdWorker.java

java
@Component
public class RedisIdWorker {
    // 起始时间戳: 2022-01-01 00:00:00 UTC
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    // 序列号占用位数
    private static final int COUNT_BITS = 32;

    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号 — 按天分key
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue()
                .increment("icr:" + keyPrefix + ":" + date);

        // 3.位运算拼接
        return timestamp << COUNT_BITS | count;
    }
}

ID 结构解析

┌─── 符号位(1bit, 始终为0) ─── 时间戳(31bit) ─── 序列号(32bit) ───┐
│ 0 │ 00000000 00000000 00000000 0000000 │ 00000000 00000000 00000000 00000000 │
└───┴──────────────────────────────────┴──────────────────────────────────────┘

注意: Java long 是64位有符号整数
- 第1位: 符号位, 始终为0(正数)
- 第2-32位: 时间戳差值(31bit), 支持 2^31 秒 ≈ 68年 (从2022到2090)
- 第33-64位: 序列号(32bit), 每天最多 2^32 ≈ 43亿个ID

使用场景

java
// 秒杀订单ID
long orderId = redisIdWorker.nextId("order");
// 对应 Redis key: icr:order:2024:03:15

// 点赞行为ID
Long behaviorId = redisIdWorker.nextId("like-behavior");
// 对应 Redis key: icr:like-behavior:2024:03:15

面试 Q&A

Q1: 为什么不用数据库自增ID?

数据库自增有三个致命问题:

  1. 单点瓶颈:所有 INSERT 都要走同一个自增序列,高并发下成为性能瓶颈
  2. 分库分表困难:多个数据库实例的自增ID会冲突
  3. 信息泄露:连续的ID暴露业务量和增长速度(竞对可以通过下两个订单推算你的单量)

追问:那为什么不用 UUID?

UUID 有两个问题:

  1. 无序性:UUID 是随机的,插入 B+Tree 索引时会导致频繁页分裂,写入性能差
  2. 长度大:128位(32个十六进制字符),作为主键浪费存储空间,JOIN 和索引查找都慢

RedisIdWorker 生成的是 64位 Long,单调递增,对 B+Tree 友好。

再追问:单调递增会不会有安全问题?ID 也是连续的啊?

不完全连续。虽然时间戳部分是递增的,但序列号部分按天重置,且不同 keyPrefix 有不同的计数器。攻击者无法通过两个 ID 差值推算精确单量,因为中间可能有其他 prefix 的 ID 生成。

如果安全要求更高,可以在 ID 生成后再做一层混淆(如对低位做异或变换)。

Q2: 为什么按天分 key?

三个好处:

  1. 避免热 key:如果全局只用一个 key,所有业务的 INCR 都打到同一个 key,成为热点
  2. 天然统计:可以通过 GET icr:order:2024:03:15 直接查当天订单量
  3. 序列号不溢出:每天重置,32位序列号(43亿/天)绰绰有余

追问:按天分 key 会不会导致跨天时刻的ID不递增?

不会。因为高32位是时间戳(秒级),跨天时刻时间戳增大,即使序列号从0重新开始,整体 ID 仍然递增。

例如:(T_day1 << 32) | 999999 < (T_day2 << 32) | 0,因为 T_day2 > T_day1

Q3: 这个方案的并发极限是多少?

理论上限由 Redis INCR 性能决定。单个 Redis 实例的 INCR 命令 QPS 可达 10万+。

按天分 key 后,每个 prefix 每天独立计数,不同业务互不影响。实际瓶颈通常不在 ID 生成,而在下游业务逻辑。

追问:如果 Redis 挂了怎么办?

这是单点问题。解决方案:

  1. Redis Sentinel / Cluster:高可用部署
  2. 降级方案:本地 Snowflake(需要预分配 workerId)作为 fallback
  3. 号段模式:提前从 Redis 批量取一段序列号缓存在本地(类似美团 Leaf)

Q4: 和 Snowflake 对比有什么优劣?

维度RedisIdWorkerSnowflake数据库自增UUID
长度64位64位32/64位128位
有序性单调递增趋势递增严格递增无序
依赖Redis机器时钟数据库
时钟回拨不受影响致命问题不受影响不受影响
每秒容量~10万(单Redis)~400万(单机)~1万无限
分布式天然支持需分配workerId需号段天然支持

RedisIdWorker 最大的优势是不受时钟回拨影响(Snowflake 的噩梦),代价是引入 Redis 依赖。


踩坑点

踩坑点说明面试官考察意图
ZoneOffset.UTC时间戳用 UTC 计算,但 seckill.lua 用的是 ZoneId.systemDefault()跨模块时区不一致
BEGIN_TIMESTAMP 硬编码1640995200 = 2022-01-01,支持到2090年左右是否考虑长期维护
无溢出保护理论上 count 超过 2^32 会溢出到时间戳位对位运算的理解
无降级机制Redis 不可用时直接抛异常系统可用性设计

加分回答

  • 对比美团 Leaf 的号段模式:预取一批 ID 到本地,减少 Redis 调用
  • 提到 Snowflake 时钟回拨问题的解决方案(NTP 关闭步进式调整、等待追回)
  • 分析 timestamp << 32 | count 的位运算:为什么用 | 不用 +(效果相同但位运算更快且语义更清晰)
  • 提到按天分 key 的副作用:Redis 中会积累大量历史 key,需要定期清理或设置 TTL

关联文档