Redis 计数器

Snowflake 实现了一周,我累了

上节我花了整整一周实现 Snowflake,调试时钟回拨、分配机器 ID、保证线程安全…虽然最终跑起来了,但我心里总有点不安:时钟回拨的隐患始终存在

那天凌晨三点,我盯着代码突然想:我只是想生成一个自增 ID,真的需要这么复杂吗?

Snowflake 的"重":
- 64 位二进制拆分,心累
- 时钟回拨处理,头大
- 机器 ID 分配,麻烦
- 线程安全保证,复杂

有没有更简单的方案?

我打开 Redis 文档,突然眼睛一亮 —— INCR 命令

原来 Redis INCR 天生就是做这个的

Redis 的 INCR 命令将 key 中存储的数字值加 1,并返回新值。关键在于:它是原子操作

我迫不及待地测试了一下:

Redis: SET url_counter 0
Redis: INCR url_counter → 1
Redis: INCR url_counter → 2
Redis: INCR url_counter → 3

  原子操作!多台机器同时 INCR 也不会冲突!

为什么 INCR 是原子的?

我深入研究了一下,发现原理很简单:Redis 是单线程执行命令的,即使多个客户端同时执行 INCR,也是排队一个个加的:

客户端 A: INCR url_counter
客户端 B: INCR url_counter  ← 等待 A 执行完
客户端 C: INCR url_counter  ← 等待 B 执行完

结果:1, 2, 3  ← 完美自增,没有冲突

这不就是我想要的分布式计数器吗?不需要时钟同步,不需要机器 ID,只需要一个 Redis 命令!

极简方案:复杂度只有 Snowflake 的十分之一

我把方案拆开后发现,它的复杂度只有 Snowflake 的十分之一:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

就这么简单!没有时钟回拨的烦恼,没有机器 ID 的分配,一个 INCR 命令搞定一切。

集成到短链接服务

我把这个方案集成到了短链接服务中:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

整个实现清爽多了!我迫不及待地上线测试,一切正常。但是…

但 Redis 挂了怎么办?

上线第二天,我突然意识到一个问题:Redis 是单点,挂了怎么办?

我的短链接服务完全依赖 Redis 生成 ID,如果 Redis 挂了,整个服务就不可用了。这比 Snowflake 的时钟回拨问题还要严重!

方案一:Redis Sentinel(哨兵模式)

我研究了一下 Redis 的高可用方案,发现了 Sentinel:

┌─────────────┐
│ Redis Master │ ← 主节点,处理所有写操作
└──────┬──────┘
       │ 复制
┌──────┴──────┐
│Redis Slave 1│ ← 从节点,随时准备接管
│Redis Slave 2│
└─────────────┘
       ↑ 监控
┌──────┴──────┐
│  Sentinel   │ × 3  ← 哨兵,监控主节点健康
└─────────────┘

接入 Sentinel 后,应用侧需要理解的变化很少:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

Sentinel 会自动监控主节点健康,如果主节点挂了,会自动选举一个从节点成为新的主节点。我的代码完全不用改!

方案二:Redis Cluster(集群模式)

如果数据量更大,我还研究了 Redis Cluster:

┌──────────┐  ┌──────────┐  ┌──────────┐
│ Master 1 │  │ Master 2 │  │ Master 3 │
│ slot 0-  │  │slot 5461-│  │slot 10923│
│  5460    │  │  10922   │  │ -16383   │
└────┬─────┘  └────┬─────┘  └────┬─────┘
     │              │              │
┌────┴────┐   ┌────┴────┐   ┌────┴────┐
│Slave 1  │   │Slave 2  │   │Slave 3  │
└─────────┘   └─────────┘   └─────────┘

不过对于短链接服务,单个 Redis + Sentinel 就够用了,Cluster 有点杀鸡用牛刀。

Redis 计数器的持久化问题

配置好高可用后,我又想到一个问题:如果 Redis 重启,计数器会不会丢?

我查了一下 Redis 的持久化机制:

RDB vs AOF

持久化方式数据安全性能影响恢复速度
RDB(快照)可能丢失几分钟数据
AOF(追加日志)最多丢 1 秒
RDB + AOF最多丢 1 秒

如果 Redis 配置了 AOF,最多只丢 1 秒的数据。但对于 ID 生成器来说,即使丢失,也只是少了一小段 ID,不会导致重复。

更安全的做法:启动时校准计数器

为了更保险,我加了一个校准逻辑:启动时从数据库查询最大 ID,确保 Redis 计数器不会比数据库小:

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

这样即使 Redis 完全丢失数据,重启后也能从数据库恢复,并且加 1 万的安全余量,绝对不会重复。

性能优化:批量预取

解决了高可用问题,我开始优化性能。Redis INCR 虽然快,但每次生成 ID 都要请求 Redis,网络开销不小。

我想到了一个优化方案:批量预取 ID

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

批量预取后,每 100 个 ID 只需要 1 次 Redis 请求,性能提升了近百倍!

不过这个方案也有个小问题:如果服务重启,预取但未使用的 ID 会丢失。但这只是浪费了一些 ID,不会导致重复,完全可以接受。

Redis 计数器 vs 雪花算法

上线一周后,我对比了两种方案:

特性Redis 计数器雪花算法
实现复杂度⭐ 非常简单⭐⭐⭐ 复杂
依赖外部服务Redis
性能10-50K QPS400 万+/秒
全局递增✅ 严格递增❌ 趋势递增
时钟依赖❌ 无依赖✅ 强依赖
高可用需要 Redis HA天然高可用
ID 长度从 1 开始,很短64 位,较长
机器 ID不需要需要分配
可读性✅ 短码短❌ 短码较长

我的最终选择

我决定在项目中同时支持两种方案

落地思路

  • 这里省略具体语法,只保留设计层面的职责边界。
  • 读这段时重点看:输入是什么、系统做哪些判断、状态如何变化、失败时如何兜底。

对于小规模应用(日生成量 < 100 万),Redis 计数器足够了;对于超大规模应用,Snowflake 更合适。

想一想

  1. Redis INCR 的性能瓶颈在哪里?单节点最多能支持多少 QPS?
  2. 如果 Redis 计数器丢失了,怎么保证新生成的 ID 不和已有的冲突?
  3. 批量预取 ID 时,如果服务重启了,预取但未使用的 ID 会怎样?