登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

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 客户端通常会把它映射成专门的事务失败错误。

Redis WATCH 从监视到 EXEC 的两种返回路径静态说明图
图1:WATCH 到 EXEC 的返回路径说明图,数组表示执行,空回复表示监视条件失效;这是静态说明图,不是运行截图。

为什么会得到空回复

WATCH 监视的是从调用它开始到 EXEC 被接收这一段时间。只要被监视的键在这段窗口内被修改,Redis 就不会执行事务队列。修改不只来自业务客户端写命令,Redis 文档还明确提到过期和淘汰也可能让监视条件失效。

要注意一个容易混淆的边界:MULTI 之后输入的命令只是入队,直到 EXEC 才执行,所以事务队列里的写命令不会在入队时触发自己的 WATCH 条件。真正需要观察的是其他客户端、过期或淘汰是否在窗口中碰了监视键。

EXEC 结果含义应用动作
数组回复事务队列已执行,逐项读取结果提交本次业务结果
空回复 / nil / nullWATCH 条件不满足,队列未执行重新读取并有限重试
数组中某一项为错误事务执行了,但某条命令单独失败按命令错误处理,不当作竞争失败

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 表示乐观锁竞争失败;调用方可以在外层循环中重新读取。其他错误则应按项目的连接重试、数据类型或业务规则处理。

Go go-redis 根据 TxFailedErr 区分提交、竞争失败和其他错误的静态流程图
图2:Go 客户端错误分流结构图,展示提交成功、WATCH 竞争失败和其他错误的处理方向;这是静态说明图,不是运行截图。

不要把命令错误当成 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 清理监视状态,再开启下一轮。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>