bigywater.tech
Redis · 31

WATCH 乐观锁

用两个独立连接,一个WATCH某个key并排队事务,另一个中途抢先修改这个key,验证EXEC是怎么检测冲突并取消整个事务的。

*-1
被冲突打断时EXEC返回的nil
[:6]
无冲突时成功提交的结果
乐观锁
WATCH只在EXEC那一刻做一次检查,不预先加锁

做的事:WATCH一个key,中途让别人抢先改一次

用两个独立的连接模拟两个并发的客户端。

代码s1, s2 = 两个独立的连接 cmd(s1, "SET", "wk", "100") cmd(s1, "WATCH", "wk") cmd(s1, "MULTI") cmd(s1, "INCR", "wk") -- 排队,还没真的执行 cmd(s2, "SET", "wk", "999") -- 另一个客户端在这时机改了wk cmd(s1, "EXEC") -- s1尝试提交

验证:EXEC返回了空数组,INCR一次都没有真的执行

这跟前两篇看到的"排队的命令EXEC时一定会执行"完全不同。

结果EXEC → *-1 (是nil,不是一个长度为1的数组) GET wk之后 → "999" (是s2改的值,s1的INCR没有生效)

关键:WATCH wk 让s1这个连接记住了"我提交事务的前提是wk这个key在我WATCH之后到EXEC之前,没有被任何人修改过"——s2抢先改了wk,Redis在EXEC执行前会检查这个前提,发现被破坏了,就取消整个事务,INCR wk这条早已排好队的命令一次都没有被执行,EXEC直接返回nil通知客户端"提交失败,你需要自己决定要不要重试"。

验证:没有干扰时,同样的流程正常提交成功

证明失败不是WATCH机制本身的问题,是检测到了冲突。

结果SET wk2 5 WATCH wk2 MULTI INCR wk2 -- 排队 (这次没有其它客户端插进来改wk2) EXEC → [:6] (是长度为1的数组,INCR执行成功,6=5+1) GET wk2 → "6"
这正是"乐观锁"的工作方式:不是真的去加一把锁阻止别人修改(那是悲观锁),而是先假设"大概率不会有人来抢",正常排队并尝试提交,只在EXEC提交那一刻做一次检查——如果WATCH的key真的被改过,就放弃这次提交,交给客户端代码自己决定重试还是放弃。这种"先检查后提交"(check-and-set)的思路,跟本系列前面讲Lua脚本原子性时对比的"GET+客户端计算+SET"两次round-trip的竞态问题是同一类根源,WATCH提供的是Redis原生支持的一种解决方案:不需要写Lua脚本,用普通的MULTI/EXEC加一句WATCH就能做到"我提交的前提是这份数据没被别人动过"。

分步演示:WATCH怎么检测冲突

每一步都是本文上面用两个独立连接跑出来的原始结果。

WATCH靠EXEC那一刻的检查实现乐观锁
这一步新验证的场景

    本文用到的工具

    • 本机 Redis 8.10.1
    • 两个独立socket连接:模拟WATCH期间被并发修改和无冲突两种场景
    京ICP备2026067502号-1京公网安备11010502063487号