导读:本期聚焦于宋琮安创作的《Redis WATCH 命令为什么是事务里的乐观锁?用法、高频问题和边界一次讲清》,敬请观看详情。WATCH 命令常被当成 Redis 事务的一个附属参数,但它其实是决定事务是否执行的关键。Redis 的 MULTI/EXEC 只负责排队和顺序执行,并不会在 EXEC 之前检查键是否被并发修改,WATCH 补上的正是这个缺口。它监控一个或多个键,一旦发现目标键在事务提交前被修改、删除或过期,EXEC 会直接返回空结果,客户端需要重新读取数据并重试。本文从 WATCH 的监控原理讲起,结合 redis-cli 与 Python 代码演示库存扣减、余额转账等典型场景,再汇总常见问题,包括键过期对事务的影响、Redis 事务不回滚的原因、集群模式下同槽限制、连接断开后的监控状态,以及 WATCH 与 Lua 脚本、分布式锁各自的适用边界。读完可以明确哪些并发更新应该用 WATCH,哪些场景需要替换成 Lua 脚本或锁方案。

Redis 的 MULTI/EXEC 只是把一组命令排队执行,它本身并不会检查键在入队后是否被其他客户端改过。如果两个客户端同时读到库存为 5,然后各自执行 DECRBY,最终库存会变成 4 还是 3,取决于谁后提交,并不会像关系型数据库那样依靠行锁规避覆盖。WATCH 命令就是用来补上这个短板的,它让 Redis 事务拥有类似乐观锁的检查能力。

Redis WATCH 命令为什么是事务里的乐观锁?用法、高频问题和边界一次讲清

WATCH 会监控一个或多个键,并在 EXEC 阶段确认这些键是否仍然保持监控开始时的状态。如果这期间有客户端修改、删除或让键过期,事务会直接失败并返回 nil,而不是继续执行。理解这一点,是掌握 WATCH 用法的关键。

WATCH 的底层逻辑:它到底监控了什么

WATCH 命令可以放在 MULTI 之前,也可以放在事务执行前任意阶段,但通常必须先于 MULTI 调用。它的语法很简单:WATCH key [key ...]。当客户端对某个键执行 WATCH 后,Redis 服务端会在内部维护一个字段,将该键标记为被该客户端监控。这个标记不是锁,而是一组元数据,Redis 利用它来跟踪键是否发生过变化。

真正发挥作用的地方在 EXEC。每次键被修改时,Redis 会记录该键的修改状态,WATCH 的客户端在执行 EXEC 时,服务端会检查监控列表中是否有任何一个键被改动。如果有,整个事务队列不会执行,EXEC 返回空结果。这里有一个容易忽略的细节:Redis 不会区分修改来自哪个客户端,哪怕修改发生在极短时间内、最终键值又被改回原值,WATCH 也会判定为已变化,事务照样失败。这避免了 ABA 问题,让并发控制更可靠。

# 终端 1
127.0.0.1:6379> WATCH stock:1001
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY stock:1001 1
QUEUED
# 此时终端 2 执行:SET stock:1001 100
127.0.0.1:6379> EXEC
(nil)

这个例子中,终端 1 的 EXEC 返回 nil,说明事务被取消。终端 2 的修改促使监控键发生了变化,终端 1 的扣减不会执行。客户端应当捕获这个结果并重新读取库存,然后再次尝试。需要特别说明的是,WATCH 的监控状态在 EXEC、DISCARD 或连接断开后都会自动取消,不会永久驻留。连接断开时,Redis 会清理该连接所有 WATCH 信息,避免垃圾监控长期占用内存。

实战:库存扣减与余额转账

库存扣减是 WATCH 的典型应用场景。业务需要读取当前库存、判断是否充足、执行扣减,这三个步骤在并发环境下必须作为一个整体条件执行。WATCH 本身不阻塞其他客户端,所以有可能多个客户端同时通过第一轮判断,但最终只有一个客户端事务成功,其他客户端需要重试。

下面用 Python 的 redis-py 演示一个带重试的库存扣减函数。逻辑上先 WATCH 商品库存键,再读取值,业务校验通过后进入 MULTI,执行 DECRBY,最后 EXEC。若返回 WatchError,说明键被并发修改,就重新走一遍流程。实际项目中需要限制重试次数,避免死循环。

import redis

r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True)

def decr_stock(item_id, amount):
    key = f"stock:{item_id}"
    for _ in range(5):
        pipe = r.pipeline()
        try:
            pipe.watch(key)
            stock = int(pipe.get(key) or 0)
            if stock < amount:
                pipe.unwatch()
                return False
            pipe.multi()
            pipe.decrby(key, amount)
            pipe.execute()
            return True
        except redis.WatchError:
            continue
    return False

上面的代码中,pipe.unwatch() 在业务校验失败时主动取消监控,避免后续无意义的 WATCH 状态。执行成功后,EXEC 会自动解除监控。另一个常见场景是账户转账:转出方和转入方都需要做余额一致性检查。可以同时 WATCH 两个账户键,任一个键在 EXEC 前被修改,整个转账事务都会取消。这样做虽然要多重试几次,但不需要引入额外的分布式锁,在低并发服务中足够简洁。

需要避免的一个做法是:只执行一次 WATCH 加 MULTI,失败后直接返回错误。这当然也能保证数据不被破坏,但当并发冲突不高时,用户会频繁看到系统繁忙请重试的提示,体验较差。推荐的做法是设置 3 到 5 次重试,并在每次重试间加入很小随机延迟,减少多个客户端同时重试的碰撞概率。若重试仍然失败,再返回业务失败或转异步队列处理。

常见问题解答汇总

EXEC 返回 nil 是什么意思?

刚接触 Redis 事务的人容易把 EXEC 返回 nil 当成命令执行失败,其实它表示事务被 WATCH 检测到冲突而取消,队列中没有任何命令被执行。如果把返回值与正常情况混在一起处理,可能导致业务误判。调用 redis-py 时,它会抛出 WatchError 异常,这比直接判断返回 nil 更直观。

WATCH 监控的键过期会让事务失败吗?

会。Redis 中键过期、被 DEL、被修改、被 RENAME 等操作都会改变键状态,WATCH 都会认为该键已被修改。因此,如果你对一个带 TTL 的缓存键使用 WATCH,要考虑到它在接近过期时可能突然触发失败。若业务允许,可以在 WATCH 之前先检查 TTL,或直接改用 Lua 脚本原子更新。

Redis 事务出错会回滚吗?

不会。Redis 事务不具备关系型数据库那样的回滚能力。它只保证在 EXEC 时命令按顺序执行,并且执行期间不会被其他客户端命令插入。如果某个命令在入队时就有语法错误,EXEC 会直接拒绝执行整批命令;但如果入队时语法正常、运行时才报错,那么只有出错的命令失败,之前的命令不会撤销。例如先 SET 一个字符串,再 INCR 它,INCR 会报错,但 SET 仍然生效。这也说明 WATCH 只能控制并发检查,不能替代业务上的合法性校验。

WATCH 可以监控 Hash 的某个字段吗?

不可以。WATCH 的最小粒度是键,而不是键内部的子结构。哪怕只修改 Hash 的某一个 field,整个键也会被判定为已修改,所有监控该键的客户端都会事务失败。如果需要字段级乐观锁,可以考虑把高频字段拆成独立的键,或者使用 Lua 脚本在服务端进行细粒度控制。

集群模式下 WATCH 有什么限制?

在 Redis Cluster 中,事务要求涉及的所有键必须落在同一个哈希槽。WATCH 多个键同样需要满足这个条件,否则会收到 CROSSSLOT 错误提示。处理跨槽业务时,可以使用 hash tag 把相关键强制映射到同一槽,例如 {order:100}:stock{order:100}:amount。但这种做法会增加单槽压力,需要根据数据规模评估。

连接断开后 WATCH 监控还在吗?

不在。WATCH 监控状态与客户端连接绑定,连接一旦断开,Redis 服务端会自动清理该连接对应的所有 WATCH 记录。这也是为什么在连接池或短连接场景下,必须保证 WATCH、MULTI、EXEC 在同一个连接上完成。如果中间从连接池换了一个连接,WATCH 根本不会生效。

WATCH、Lua 脚本和分布式锁的边界

WATCH 适合低并发、可快速重试的业务,它把判断逻辑留在客户端,灵活但需要处理冲突重试。Lua 脚本则把判断与更新放进 Redis 服务端一次执行,天然具备原子性,不需要重试,但脚本执行期间会占用 Redis 单线程,脚本复杂或执行时间长会阻塞其他命令。两者并不冲突,很多系统用 Lua 脚本来替代简单的 WATCH 加 MULTI 组合,尤其适合库存扣减、排行榜更新这类逻辑固定且耗时不长的操作。

分布式锁则是另一种思路,它通过 SET NX PX 等命令在客户端之间建立互斥,拿到锁的客户端才允许执行读取和更新。相比 WATCH,分布式锁能控制更复杂的业务流程,比如多个 Redis 操作、外部 API 调用等,但也带来锁超时、锁误删、主从切换锁丢失等问题。如果业务只涉及单个或少数几个 Redis 键的原子更新,WATCH 或 Lua 脚本通常更简单;如果涉及缓存、数据库、消息队列等多步外部调用,才需要考虑分布式锁。

综合来看,WATCH 是 Redis 原生提供的一种轻量乐观锁机制,理解它的监控粒度和失败语义,能帮助你在不使用额外组件的情况下解决一部分并发更新问题。但也要记住,它不是万能方案,Redis 事务不回滚、集群同槽限制、键过期触发失败等特性,决定了它在复杂业务中需要与 Lua 脚本、分布式锁甚至应用层队列配合使用。把 WATCH 放在合适的场景里,代码会明显更简单、更可靠。

Redis WATCH命令乐观锁事务修改时间:2026-09-04 01:45:55

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260904/49936.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。