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。