day34 全局唯一ID与优惠券秒杀

1. 今天做了什么

  • 学习优惠券秒杀场景下的全局唯一 ID 生成方案
  • 使用 Redis 自增数值实现全局唯一 ID(RedisIdWorker)
  • 深入分析 LocalDateTime 与时区的关系
  • 了解 CountDownLatch 的用法
  • 了解其他全局 ID 生成方案(UUID、雪花算法)

2. 核心知识点

数据库主键自增的缺点:

  1. 规律性强,暴露订单量,不安全,不适合作为订单号
  2. 数据库单表数量限制,分表存储时 id 无法保证唯一

使用 Redis 自增数值实现全局唯一 ID:

  • id 采用 64 位整型数值存储,Java 中使用 long 存储
  • 1 位符号位 + 31 位秒时间戳 + 32 位序列号
  • 符号位为 0 表示 id 始终为正数
  • 秒时间戳:当前秒数 - 一个指定起始时间的秒数
  • 32 位序列号:使用 Redis 的自增数值来实现
  • 时间戳左移 32 位,与序列号做与运算

计算起始时间戳:

1
2
3
4
public static void main(String[] args) {  
long epochSecond = LocalDateTime.of(2026,1,1,0,0,0).toEpochSecond(ZoneOffset.UTC);
System.out.println(epochSecond);
}

需要注意的点:

  • LocalDateTime.now() 按系统默认时区取当前本地时间
  • 注意起始时间与现在时间需要使用同一个 ZoneOffset 来转秒数时间戳

CountDownLatch:

名为信号枪,主要作用是同步协调多线程的等待与唤醒。让一个或多个线程,等待其他一组线程完成某些操作后再继续执行。

方法 作用
await() 阻塞当前线程,直到计数器归零;如果已经是 0,立即返回
countDown() 计数器减 1;减到 0 时唤醒所有等待线程

其他全局 ID 生成方案:

  1. UUID

    • 缺点:无法保持递增,是字符串类型
  2. 雪花算法

    • 与上述方案类似
    • 趋势递增,long 数值类型
    • 本地生成,不依赖 Redis,性能非常高
    • 缺点:
      1. 依赖时钟:如果服务器时钟回拨,可能生成重复 ID。因为序列号是毫秒的序列号,每毫秒重置,且仅仅 4096 个,所以确定唯一性的重要依据就是毫秒时间戳,时间回拨极有可能与同一机器码生成的 id 重复
      2. 需要维护机器 id
      3. 不绝对递增

雪花算法 ID 结构:

1
0 | 41 位时间戳 | 10 位机器 ID | 12 位序列号(同一毫秒的序列号)

3. 深度思考

关于时间戳的深入分析:

  1. LocalDateTime.now() 拿到的是北京时间墙上读数,不带时区,比真实 UTC 多 8 小时。
  2. LocalDateTime 没有时区,toEpochSecond(offset) 必须指定偏移,表示”把这串本地读数当作该偏移下的时间”去算时间戳。
  3. now.toEpochSecond(ZoneOffset.UTC) 是把北京时间当成 UTC,算出的时间戳比真实 UTC 多了 8 小时。
  4. now.toEpochSecond(ZoneOffset.ofHours(8)) 才是真实当前 UTC 时间戳。
  5. 求”现在到北京时间 2026-01-01 00:00:00”的差值,只要两个时间用同一个偏移转成时间戳再相减,偏移会抵消,所以用 UTC 或 +08:00 结果一样。
  6. 原代码里 START_TIMESTAMP 名义是 UTC 2026 元旦,但 now 也按 UTC 转,两者相减后,实际就是北京时间 2026 元旦到现在的秒数。
  7. date 用的也是 now 的北京读数,所以 Redis key 按北京日期分片。

4. 踩过的坑

5. 遗留问题 / 想不通