2026年7月13日 · 7 分钟阅读

Redis 并发控制与生产治理:原子操作、Lua、锁、Hot Key 与 Big Key

从 Redis 单命令原子性出发,梳理事务、WATCH、Lua 和分布式锁的正确边界,并总结 Hot Key、Big Key、慢命令、容量与生产部署治理。

Redis 命令主要由单线程串行执行,但这只意味着单条命令执行时不会被另一条命令插入,不代表客户端的“读取—判断—写入”业务流程天然原子。

客户端 A:GET stock → 1
客户端 B:GET stock → 1
客户端 A:SET stock 0
客户端 B:SET stock 0

两次请求都认为扣减成功。并发正确性必须通过原子命令、事务、Lua 或带版本的业务协议显式设计。

优先使用原子命令

能用单条命令表达时,不要拆成多条:

  • 计数使用 INCRDECR,而不是 GETSET
  • 仅不存在时写入使用 SET key value NX EX seconds
  • 集合成员关系使用 SADDSREM
  • 排行和计分使用 ZINCRBY
  • 条件删除锁值使用 Lua,而不是先 GETDEL

还要检查业务语义。例如 DECR 虽然原子,却不能自动阻止库存减为负数;“库存大于零才扣减”需要把判断和修改放在同一个原子单元。

MULTI/EXEC 与 WATCH

Redis 事务把命令排队后由 EXEC 连续执行,执行期间不会插入其他客户端命令,但它不像关系数据库事务那样提供通用回滚。

WATCH 提供乐观 CAS:被监视的 key 在 EXEC 前发生变化,事务会放弃,客户端重新读取并重试。

WATCH stock
current = GET stock
if current > 0:
    MULTI
    DECR stock
    EXEC   # 冲突时返回空结果,应用有限重试

高竞争热点下 WATCH 会频繁失败,重试放大流量。这时 Lua 或重新设计数据模型通常更合适。

Lua:把多步逻辑放到服务端原子执行

local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock <= 0 then
  return 0
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1

Lua 脚本执行期间会阻塞其他命令,因此必须短小、确定且限制输入规模。不要在脚本中遍历大集合或执行不可控循环。Redis Cluster 中相关 key 还需位于同一 hash slot,通常通过 hash tag 规划。

分布式锁的正确边界

最小锁模式是带唯一 token 和过期时间的条件写入:

SET lock:order:42 <random-token> NX PX 10000

释放时用 Lua 原子比较 token 后删除,防止客户端 A 的锁过期后误删客户端 B 的新锁。

但租约锁仍有边界:持有者可能发生长时间 GC、网络暂停或业务耗时超过租约,旧持有者恢复后与新持有者同时修改资源。关键资源应让数据源验证单调递增 fencing token,拒绝旧持有者的写入。

如果任务可通过数据库唯一约束、状态机、幂等键或消息分区串行化解决,通常优先这些方案。锁不是一致性的万能开关。

Hot Key:单分片流量热点

Hot Key 是访问频率远高于其他 key 的单个键。Redis Cluster 中一个 key 只属于一个分片,因此增加集群总节点不一定能分散它。

识别信号包括:单分片 CPU/网络显著偏高、命令采样集中、redis-cli --hotkeys 发现高频键。生产环境谨慎使用 MONITOR,它可能带来显著额外开销。

治理方式:

  • 只读热点使用应用本地缓存和短 TTL。
  • 可拆分计数按分片累加,读取时聚合。
  • 对确定的热点提前预热并避免同时过期。
  • 限制单租户或单 key 的请求速率。
  • 检查路由键是否造成错误的数据倾斜。

Big Key:单键数据过大

Big Key 可能是数百 KB 的 String,也可能是包含几十万元素的 Hash、Set 或 ZSet。风险包括网络突刺、序列化耗时、主线程长时间阻塞、复制和迁移困难、删除抖动。

可用 redis-cli --bigkeys--memkeysMEMORY USAGE 和抽样扫描识别。不要在生产高峰执行无边界 KEYSSMEMBERSHGETALL

治理方式是按业务维度拆 key、分页/游标读取、限制 value 大小和集合元素数;删除大 key 优先 UNLINK 异步回收内存。大文件和大 JSON 通常应放对象存储或数据库,Redis 只缓存索引与热点片段。

Pipeline、批量与连接池

Pipeline 通过一次网络往返发送多条命令,提高吞吐,但不提供事务原子性。批次过大会增加客户端和服务端缓冲、尾延迟与失败重试成本,应通过压测选择上限。

连接池不是越大越好。过多连接会放大 Redis 和网络压力。应设置:

  • 连接、命令和池等待超时。
  • 最大连接与最小空闲连接。
  • 有界重试和随机退避。
  • 熔断与请求总时间预算。

超时后命令可能已经执行,写操作重试必须具备幂等性。

慢命令与延迟治理

Redis 的快来自短命令串行执行。一个 O(N) 大操作会阻塞同一执行线程上的其他请求。治理工具包括:

  • SLOWLOG 定位超过阈值的命令。
  • LATENCY LATEST/HISTORY/DOCTOR 分析命令、fork、淘汰等延迟事件。
  • INFO commandstats 查看命令调用量和耗时。
  • 客户端监控端到端 P95/P99,而不只看服务端执行时间。

Redis 官方也明确建议生产环境避免 KEYS,使用 SCAN 系列渐进遍历。还要关注 fork、AOF fsync、内存换页、Transparent Huge Pages 和网络抖动。

容量、可靠性与安全

生产部署要形成闭环:

  1. 按工作集、对象开销、碎片率、副本和增长量估算内存。
  2. 设置 maxmemory 和符合业务的淘汰策略,给复制、fork 和系统留余量。
  3. 根据 RPO/RTO 选择 RDB、AOF、主从、哨兵或 Cluster,并定期验证恢复。
  4. 监控内存、eviction、命中率、连接、拒绝、复制延迟、持久化和分片倾斜。
  5. 使用私网、ACL、最小权限、TLS 和密钥轮换,禁用危险命令不是唯一安全边界。
  6. 做节点宕机、网络分区、缓存冷启动、Hot Key 和 Big Key 演练。

现有的持久化、过期淘汰和高可用文章解释了各机制内部原理;生产设计需要把它们按业务恢复目标组合起来。

上线检查清单

  • 多步读改写是否已改为原子命令、WATCH 或短 Lua。
  • 锁是否有唯一 token、租约、原子释放和超时后的安全策略。
  • key/value 大小和集合元素数是否有硬限制。
  • 是否监控 Hot Key、Big Key、慢命令和分片倾斜。
  • 客户端是否设置完整超时、有界连接池和幂等重试。
  • 内存是否为复制、持久化和故障切换预留空间。
  • 备份是否真正做过恢复演练。

面试回答要点

回答 Redis 并发问题时,先澄清“单线程只保证单命令原子”,再按单命令、WATCH 事务、Lua 和业务幂等依次选择。谈分布式锁要主动说明 token、租约和 fencing 边界。生产问题则从 Hot Key、Big Key、慢命令、客户端超时、容量、高可用、安全和演练展开。

总结

Redis 的并发正确性来自原子操作和明确协议,而不是“单线程”三个字。生产稳定性则取决于数据模型、命令复杂度、热点分布、客户端时间预算、内存水位和恢复演练。把这些边界提前固化为规范,才能让 Redis 从一个快组件变成可治理的基础设施。

参考资料