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

Redis WATCH 怎样在并发修改时放弃事务

来源:17golang原创

时间:2026-10-18 10:56:31 486浏览 收藏

Redis 的 WATCH 让 EXEC 带上条件:从 WATCH 建立监视到 EXEC 检查时,只要任一被监视键发生了会使监视失效的修改,EXEC 就返回空回复,整组排队命令都不执行。这不是执行失败后回滚,而是在执行前放弃本次事务。

如果我要根据读到的旧配置计算一个新配置,我会先考虑 WATCH:竞争较少时,允许别人先修改,自己发现冲突后重读即可。但如果只是计数加一,我会直接选 INCR,而不是为了“用了事务”增加往返与重试。本文比较的是这类取舍,不声称发生过某次实际线上事故。

官方地址:https://redis.io/docs/latest/commands/watch/。以下双连接示例供读者在隔离测试实例操作,本文未执行命令,结果描述均为预期。

我会先确认是冲突,还是命令错误

日志里出现“事务没成功”时,我不会立即增加重试次数,而会先分清 EXEC 的返回类型:

观察到的结果含义处理方向
EXEC 空回复WATCH 条件失效,本次队列未执行重新读取并计算,按预算重试
EXEC 返回数组,某个元素是错误进入执行阶段,某条命令报错逐项处理,不能假定其他写入撤销
EXECABORT 等命令级错误例如入队阶段错误导致事务被拒绝修正命令或环境,不当普通竞争重试
超时或连接断开,没有确定回复客户端不知道最终结果不能直接归类为 WATCH 冲突

空回复也不等于“成功执行了零条命令”的空数组。客户端可能把冲突表示为专门异常或状态,应用应遵循所用驱动的返回约定,而非用列表是否为空或日志是否有文字判断。

用两个持久连接看清中止条件

读者需要两个长期保持的客户端连接 A、B,连接到同一测试实例和数据库。在两个现有命令会话中分别进入 redis-cli:

# 在两个独立的命令会话中各执行一次,后续命令在各自 redis-cli 内输入
# 仅连接隔离测试实例;不要在业务实例上初始化示例键
redis-cli

确认专用样例键可用于测试后,按下表人工交错输入。表中“暂停”是操作提示,不是 Redis 命令;不要把两列一次粘贴到同一连接。

操作位置输入或动作预期观察
B:准备数据SET article:watch:config v1设置已知初值
A:建立监视WATCH article:watch:configOK,只表示建立监视
A:读取输入GET article:watch:config,随后暂停读到 v1
B:并发改写SET article:watch:config v2B 可以正常写入,不被 WATCH 阻塞
A:准备事务MULTI,再输入 SET article:watch:config v3分别回复 OK、QUEUED
A:提交EXEC空回复,不会执行这次 SET
A:教学观察GET article:watch:config没有其他写入时仍为 v2

关键不在于 v1、v2 是否不同,而在于监视期间是否发生修改。WATCH 不是在最后把两个字符串做一次相等比较。QUEUED 也只表示命令已入队,不能当成更新成功。

这个结构中,连接 A 持有监视状态和事务队列,二者都关联同一个被监视键;连接 B 的写命令是外部修改来源,EXEC 是条件提交的入口。图中的连线只表达这些归属和依赖。

两个连接、监视状态、事务队列与被监视键的静态关系
图1:静态说明图。连接 A 的监视与队列属于同一连接边界,连接 B 仍可修改键;不表示操作时间线或运行截图。

连接边界比重试次数更重要

我会先检查 WATCH、读取、MULTI、EXEC 是否由同一条连接完成。逐次运行独立的 redis-cli WATCH ...、redis-cli GET ... 并不能拼成一次事务:前一条进程退出,监视连接也结束了。

应用连接池同样需要显式事务会话或连接绑定,不能把这几条调用随意分配给不同连接,也不能让其他请求共享处于监视或事务状态的连接。若更新依赖多个键,应在读取这些依赖之前监视所有相关键;只监视最终写入键,不能保护遗漏的读取条件。

不继续操作且尚未进入 MULTI 时,用 UNWATCH 清除监视;已经进入 MULTI 而决定放弃,用 DISCARD 清空队列并结束事务,同时取消监视。不要在排队后用 UNWATCH 充当事务取消命令。EXEC 无论成功还是因冲突放弃,都会清除监视;连接关闭也会清除。

连接归还池之前,应由驱动完成明确清理;若协议状态已不确定,我会淘汰该连接,不把“可能还在事务里”的连接交给下一个请求。

冲突后重读重算,不复用旧写入

收到确定的 WATCH 冲突后,我会从新的 WATCH 开始,重新读取当前值,再重新计算和排队。只把第一次算出的 v3 再提交一次,虽然可能不再冲突,却已失去“基于当前输入计算”的意义。

清理状态、重读重算、重试预算应是三个独立约束:设置尝试次数或截止时间,竞争持续时加入有界退避,超过预算就把冲突交给调用方。重试循环内不要提前发送邮件、扣外部账户或调用不可撤销服务,否则 Redis 队列虽未执行,外部副作用却可能重复发生。

网络超时需要另一条处理路径:如果 EXEC 已送达但回复丢失,事务可能已经执行。它不是确定的空回复,盲目重跑非幂等操作可能重复更新。应结合业务标识和结果确认机制处理,而不是统一捕获所有错误后继续循环。

EXEC 回复与冲突、逐项错误、结果未知处理的静态关系
图2:静态说明图。不同回复类型对应不同判断依据,只有确定冲突才能归入本次队列未执行;不是自动重试流程图。

WATCH 不负责执行错误回滚

我不会把 WATCH 介绍成数据库式的“出错全部撤销”。如果监视条件通过,EXEC 开始执行,某条命令因类型不符报错,其他排队命令仍会被处理。此时应检查回复数组的每一项,而非只检查 EXEC 调用有没有返回。

另一个边界是过期:Redis 自身的过期、淘汰也可能使监视失效。Redis 6.0.9 之前,被监视键过期不导致事务中止,这一历史差异不能忽略。测试若带有效期,应记录版本与 TTL,不要把服务器自身修改误判成“莫名其妙的并发写入”。

WATCH 监视的不是一个只允许别的客户端触发的锁。监视客户端在事务之外对键的修改也可能使自己的监视失效;MULTI 中的命令在 EXEC 前只是排队,不能与立即写入混为一谈。

按业务约束选择,不把 WATCH 当互斥锁

需求我会优先考虑需要接受的代价
单纯原子加一INCR使用命令已有的整数语义
应用基于读取结果计算再更新WATCH 配合同连接事务竞争时重读重算,有重试预算
固定一组命令,无条件读取依赖MULTI / EXEC仍要处理每条命令错误,不承诺回滚
禁止其他写入者进入业务临界区另行设计协调机制WATCH 不会阻塞对方写入

别人修改后又改回原值,还会中止吗?

会发生监视失效;它检测监视期间的修改,而不是只比较最终内容。需要值相等语义时,不应把 WATCH 当成单纯的字符串比较函数。

只 WATCH 没有写入的键有意义吗?

有。只要那个键影响更新决策,就应成为读取依赖的一部分。被监视键不要求一定出现在事务的写命令中。

冲突后直接再 EXEC 可以吗?

不行。本次事务已经结束,监视也已清除。应重新监视、读取、计算和建立事务,而不是重复发 EXEC 或复用旧计算结果。

我的选择条件是:能固定同一连接、读取依赖清楚、竞争可接受且能重算时,WATCH 才是合适的乐观并发控制;无法满足这些条件时,增加重试次数不会修复模型本身。

官方参考:事务语义 https://redis.io/docs/latest/develop/using-commands/transactions/;EXEC 返回类型 https://redis.io/docs/latest/commands/exec/;取消排队 https://redis.io/docs/latest/commands/discard/;取消监视 https://redis.io/docs/latest/commands/unwatch/。

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