首页/数据/数据库速查/WATCH 乐观锁(CAS 与重试)
Redis 运维与场景124 条

WATCH 乐观锁(CAS 与重试)

WATCH 监视若干 key;若在 EXEC 前任一 key 被其他客户端修改,EXEC 返回 nil 表示事务放弃,客户端需重读重试——即 CAS/乐观锁模式。

数据库速查数据更新于 2026-10-13

WATCH 的乐观锁语义

WATCH 监视一批键,如果在 EXEC 执行之前任何一个被监视的键被其他客户端改动,整个事务就会被放弃,EXEC 返回空表示什么都没执行。这就是 CAS 模式:读值、判断、提交,提交失败就重读重算。它把冲突检测的职责交给数据库,客户端只需要负责重试循环,逻辑写起来比 Lua 脚本更直观。

基本信息

适用平台Redis 全版本的事务机制
相关命令WATCH、UNWATCH、MULTI、EXEC、DISCARD
失败信号EXEC 返回 nil 表示被监视键被修改过
生命周期监视在 EXEC、UNWATCH 或连接关闭后清除
重试责任完全由应用侧实现

典型使用场景

  • 库存扣减:先读余量判断后再扣,冲突时重试
  • 配置项的条件更新:只有值仍是期望版本时才写入
  • 分布式计数器:多客户端竞争同一键时保证不丢更新
  • 状态机流转:确认状态未被其他流程改变再推进
  • 替代简单 Lua 脚本:逻辑复杂但冲突率不高的场景

CAS 扣减库存

CAS 扣减库存
WATCH stock:1001
GET stock:1001            # 读到 5,客户端判断可以扣减
MULTI
DECRBY stock:1001 1
EXEC                      # 返回 nil 说明期间有人改过这个键
# 此时回到 WATCH 重新读取、重新判断、重新提交
# 伪代码:for i in range(3): if cas_decr(): break; else: 随机退避重试
UNWATCH                   # 提前放弃时主动清掉监视

语义要点

要点说明注意
监视范围仅限被 WATCH 的键未监视的键被改不会导致失败
失败时机EXEC 执行前检测检测通过则原子执行
自身修改自己修改监视键同样触发失败重试逻辑要覆盖这条路径
监视清除EXEC、UNWATCH、连接关闭连接归还连接池前要确认状态干净
并发冲突冲突越多失败越多高竞争场景改用 Lua 更合适
高竞争场景下 WATCH 的重试会明显增多,此时用 Lua 脚本把判断与修改放到服务端一次完成更合适;WATCH 的优势在于逻辑留在客户端,便于调试与复用。
重试必须有次数上限与随机退避:无节制的立即重试在竞争激烈时会让客户端 CPU 空转,并把请求压力成倍放大到 Redis。

易错点

  • 只处理「别人改了」的情况:自己写回同名键也会触发失败,重试逻辑漏掉这条路径会莫名报错
  • 不给重试设上限:高并发下不断重试形成 CPU 空转与流量放大
  • 把 WATCH 的键放在连接池复用的连接上:残留的监视状态污染后续请求,必须先 UNWATCH
  • 在极高竞争场景硬用 WATCH:失败率居高不下,改用 Lua 或原子命令更划算

数据来源与参考

  • Redis 官方文档: WATCH
  • Redis 官方文档: Transactions

相关工具

​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌