1. 今天做了什么
- 学习优惠券秒杀场景下的全局唯一 ID 生成方案
- 使用 Redis 自增数值实现全局唯一 ID(
RedisIdWorker) - 深入分析
LocalDateTime与时区的关系 - 了解
CountDownLatch的用法 - 了解其他全局 ID 生成方案(UUID、雪花算法)
2. 核心知识点
数据库主键自增的缺点:
- 规律性强,暴露订单量,不安全,不适合作为订单号
- 数据库单表数量限制,分表存储时 id 无法保证唯一
使用 Redis 自增数值实现全局唯一 ID:
- id 采用 64 位整型数值存储,Java 中使用
long存储 - 1 位符号位 + 31 位秒时间戳 + 32 位序列号
- 符号位为 0 表示 id 始终为正数
- 秒时间戳:当前秒数 - 一个指定起始时间的秒数
- 32 位序列号:使用 Redis 的自增数值来实现
- 时间戳左移 32 位,与序列号做与运算
计算起始时间戳:
1 | public static void main(String[] args) { |
需要注意的点:
LocalDateTime.now()按系统默认时区取当前本地时间- 注意起始时间与现在时间需要使用同一个
ZoneOffset来转秒数时间戳
CountDownLatch:
名为信号枪,主要作用是同步协调多线程的等待与唤醒。让一个或多个线程,等待其他一组线程完成某些操作后再继续执行。
| 方法 | 作用 |
|---|---|
await() |
阻塞当前线程,直到计数器归零;如果已经是 0,立即返回 |
countDown() |
计数器减 1;减到 0 时唤醒所有等待线程 |
其他全局 ID 生成方案:
UUID
- 缺点:无法保持递增,是字符串类型
雪花算法
- 与上述方案类似
- 趋势递增,
long数值类型 - 本地生成,不依赖 Redis,性能非常高
- 缺点:
- 依赖时钟:如果服务器时钟回拨,可能生成重复 ID。因为序列号是毫秒的序列号,每毫秒重置,且仅仅 4096 个,所以确定唯一性的重要依据就是毫秒时间戳,时间回拨极有可能与同一机器码生成的 id 重复
- 需要维护机器 id
- 不绝对递增
雪花算法 ID 结构:
1 | 0 | 41 位时间戳 | 10 位机器 ID | 12 位序列号(同一毫秒的序列号) |
3. 深度思考
关于时间戳的深入分析:
LocalDateTime.now()拿到的是北京时间墙上读数,不带时区,比真实 UTC 多 8 小时。LocalDateTime没有时区,toEpochSecond(offset)必须指定偏移,表示”把这串本地读数当作该偏移下的时间”去算时间戳。now.toEpochSecond(ZoneOffset.UTC)是把北京时间当成 UTC,算出的时间戳比真实 UTC 多了 8 小时。now.toEpochSecond(ZoneOffset.ofHours(8))才是真实当前 UTC 时间戳。- 求”现在到北京时间 2026-01-01 00:00:00”的差值,只要两个时间用同一个偏移转成时间戳再相减,偏移会抵消,所以用 UTC 或 +08:00 结果一样。
- 原代码里
START_TIMESTAMP名义是 UTC 2026 元旦,但now也按 UTC 转,两者相减后,实际就是北京时间 2026 元旦到现在的秒数。 date用的也是now的北京读数,所以 Redis key 按北京日期分片。