Redis WATCH 监视键变化后怎样判断事务放弃
来源:17golang原创
时间:2026-11-11 20:20:19 118浏览 收藏
用 Redis 的 WATCH 做乐观锁时,事务是否真正执行,不能看 MULTI 后每条命令都返回了 QUEUED,而要看最后的 EXEC 回复。EXEC 返回数组,说明队列中的命令已经按顺序执行;如果返回空回复(RESP2 中常见为 nil,RESP3 中为 null),说明监视键在执行前发生了变化,事务被放弃。
判断 Redis WATCH 事务是否放弃的核心只有一个:读取 EXEC 的最终返回值。数组是“执行了”,空回复是“因 WATCH 条件不满足而没有执行”。不要把 EXEC 数组中的单条命令错误误判成 WATCH 事务放弃。
我在把库存扣减从单次写入改成“读取、计算、条件写入”时,最容易犯的错就是只判断客户端有没有报网络错误。真正的竞争失败通常没有连接异常,Redis 只是返回一个空的 EXEC 结果,应用必须把它识别为“重读后再试”的信号。
官方地址:https://redis.io/docs/latest/develop/using-commands/transactions/
本文只讨论单个客户端连接上的 WATCH、MULTI、EXEC 判断,不展开 Cluster 多键约束,也不把 Redis 事务当成数据库回滚事务。
先看 EXEC 返回什么
Redis 官方事务文档把 WATCH 描述为一种类似 CAS 的乐观锁:从执行 WATCH 开始,到服务器收到 EXEC 之前,如果至少一个被监视键发生修改,整个事务会中止,EXEC 返回空回复。
没有竞争时,EXEC 返回一个数组,数组里的每一项对应事务队列中的一条命令。下面的命令序列展示了两种最终形状:
# 先设置业务键,便于观察事务结果 SET stock:item-7 10 # 没有其他客户端改动时,EXEC 返回命令结果数组 WATCH stock:item-7 GET stock:item-7 MULTI SET stock:item-7 9 EXEC # 典型结果:数组中有 SET 的执行结果 1) OK # 如果另一个客户端在 WATCH 与 EXEC 之间改了 stock:item-7 # EXEC 的典型结果是空回复,而不是一个包含错误字符串的数组 WATCH stock:item-7 GET stock:item-7 MULTI SET stock:item-7 8 EXEC (nil)
上面的注释是为了说明返回形状;不同客户端会把数组和空回复映射成不同的语言对象,但语义不变。命令行客户端可能显示 (nil),Go 客户端通常会把它映射成专门的事务失败错误。

为什么会得到空回复
WATCH 监视的是从调用它开始到 EXEC 被接收这一段时间。只要被监视的键在这段窗口内被修改,Redis 就不会执行事务队列。修改不只来自业务客户端写命令,Redis 文档还明确提到过期和淘汰也可能让监视条件失效。
要注意一个容易混淆的边界:MULTI 之后输入的命令只是入队,直到 EXEC 才执行,所以事务队列里的写命令不会在入队时触发自己的 WATCH 条件。真正需要观察的是其他客户端、过期或淘汰是否在窗口中碰了监视键。
| EXEC 结果 | 含义 | 应用动作 |
|---|---|---|
| 数组回复 | 事务队列已执行,逐项读取结果 | 提交本次业务结果 |
| 空回复 / nil / null | WATCH 条件不满足,队列未执行 | 重新读取并有限重试 |
| 数组中某一项为错误 | 事务执行了,但某条命令单独失败 | 按命令错误处理,不当作竞争失败 |
Go 客户端怎样判断事务放弃
在 Go 项目中,github.com/redis/go-redis/v9 的事务辅助方法会把 WATCH 条件失败表示为 redis.TxFailedErr。判断顺序应该是先区分这个错误,再处理连接错误、脚本错误或业务命令错误。
package main
import (
"context"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
)
func reserveOne(ctx context.Context, client *redis.Client, key string) error {
// Watch 回调会在同一连接上完成读取、入队和 EXEC。
return client.Watch(ctx, func(tx *redis.Tx) error {
current, err := tx.Get(ctx, key).Int()
if err != nil {
return err // 键不存在或连接异常时交给调用方判断
}
if current
这段代码的关键不在于把错误字符串打印出来,而在于保留错误类别。redis.TxFailedErr 表示乐观锁竞争失败;调用方可以在外层循环中重新读取。其他错误则应按项目的连接重试、数据类型或业务规则处理。

不要把命令错误当成 WATCH 放弃
Redis 事务没有传统数据库那种回滚机制。即使 EXEC 返回数组,数组中的某一条命令也可能返回错误;Redis 不会因为其中一条命令执行失败就停止处理其他已入队命令。这和 WATCH 条件失效是两种完全不同的情况。
# 下面的事务可以成功进入 EXEC,但第二条命令会因类型不对而报错 SET demo:value hello MULTI SET demo:value world LPOP demo:value EXEC # 典型语义:第一条成功,第二条单独返回 WRONGTYPE 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
因此可以按这个顺序做判断:第一层看客户端是否拿到了连接或协议错误;第二层看 EXEC 是否为空;第三层如果是数组,再逐项检查每条命令的结果。只有第二层的空回复才代表“因 WATCH 监视键变化而放弃”。
重试要有限,竞争太高时换方案
竞争失败后通常需要重新读取最新值,再重新开始 WATCH。不要复用已经执行过 EXEC 的连接状态,也不要在不设上限的情况下无限循环,否则热点键会把请求堆在重试路径上。
func reserveWithRetry(ctx context.Context, client *redis.Client, key string) error {
const maxAttempts = 5
for attempt := 1; attempt
如果一个热点键长期出现 TxFailedErr,说明用 WATCH 做的读改写竞争过强。能用单条原子命令解决时,优先换成对应命令;如果业务逻辑复杂,也可以评估 Redis 脚本。方案选择应以业务是否需要读取后的条件判断为准,而不是单纯增加重试次数。
常见问题
EXEC 返回空数组是不是事务放弃? 不是。空回复(nil/null)才是 WATCH 条件失败的信号;空数组通常表示事务执行了但队列里没有命令,具体仍以客户端返回类型为准。
WATCH 失败时会执行 MULTI 里的 SET 吗? 不会。监视键在 EXEC 前被修改时,整个事务队列被放弃。
EXEC 数组里出现 error,是否说明 WATCH 失败? 不是。这表示事务已经执行,某条命令在执行阶段出错;需要逐项处理。
重试前要不要调用 UNWATCH? 通过 go-redis 的 Watch 回调返回后,客户端会结束本次事务流程;如果手工管理连接,可使用 EXEC 或 UNWATCH 清理监视状态,再开启下一轮。
-
278 收藏
-
483 收藏
-
291 收藏
-
195 收藏
-
412 收藏
-
391 收藏
-
487 收藏
-
263 收藏
-
317 收藏
-
471 收藏
-
314 收藏
-
233 收藏
-
317 收藏
-
182 收藏
-
135 收藏
-
374 收藏
-
223 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习