news 2026/10/14 23:16:56

Redis事务机制详解:MULTI、EXEC、WATCH与回滚策略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis事务机制详解:MULTI、EXEC、WATCH与回滚策略

Redis 的事务(Transaction)在官方文档里通常就被四个命令概括:MULTI、EXEC、DISCARD、WATCH。可一旦真在业务里跑起来,很多人会发现它和我们习惯的关系型数据库事务差得不是一点半点。这篇文章就把这四个命令拆开揉碎了聊一遍:Redis 事务到底是怎么设计的、命令怎么配合、什么时候会静默失效、以及遇到“需要回滚”的场景应该如何换思路。适合已经掌握 Redis 基础命令、准备在关键业务里引入事务,或者面试前想把底层行为彻底搞清楚的同行参考。

1. Redis 事务是什么:先看核心设计和定位

1.1 事务到底要解决什么问题

Redis 事务的初衷并不复杂:让多个命令作为一个整体,在 Redis 服务端按顺序执行,并且执行过程中不会被其他客户端的命令插入。举一个最常见的例子:一次库存扣减,往往要同时做“扣减库存数量”和“写入扣减记录”。这两条命令分开发送,中间如果插进来一个查询或者别的写入,虽然不一定出错,但逻辑边界就模糊了;更麻烦的是如果客户端在第一条命令成功后、第二条命令前崩溃,数据就只剩了一半。

用 MULTI/EXEC 包住这两条命令,就能避免这种“中间插入”和“执行一半”的尴尬。Redis 是单线程处理命令的,事务一旦开始 EXEC,队列里的命令会被逐一执行,在事务执行完毕之前,其他客户端的请求只能排队等着。所以 Redis 事务提供的核心能力是:把多个动作打包成一个不可被插队的整体。

需要明确的是,这个“整体”不等于数据库里的 ACID 事务。它更像是一个“串行执行队列”,而不是“要么全做、要么全不做”的原子操作。

1.2 它和关系型数据库事务的三处关键差异

很多人在 MySQL 里习惯了事务,下意识认为 Redis 事务也应该有回滚能力,这是最容易踩坑的地方。三处关键差异必须提前说清楚:

方面关系型数据库事务Redis 事务
原子性支持回滚,出错后可以恢复到事务开始前不支持回滚,运行时错误不会撤销之前已执行的命令
隔离性需要通过隔离级别控制可见性单线程事件循环天然隔离,事务内命令不会被插队
持久性通常在事务提交时有完整持久化保障取决于 RDB / AOF 配置,Redis 自身不保证每笔事务都立即落盘

所以,如果你需要的功能是“先检查库存,再扣库存,如果中间失败就把前面的扣减还原”,直接用 Redis 原生的 MULTI/EXEC 做不到。这不是 Redis 不完整,而是它的设计思路本来就不一样。Redis 更推荐用 WATCH 做乐观锁,或者在需要复杂控制和回滚时改用 Lua 脚本。

2. 四个核心命令的语法和细节

2.1 MULTI:事务从这里开始

MULTI 是整个事务的入口。客户端发送 MULTI 后,连接会进入事务状态,Redis 会返回 OK。从这一刻起,后续所有命令都不会被立即执行,而是被塞进一个命令队列,Redis 对每条入队命令返回 QUEUED。

> MULTI OK > SET product:1001:stock 5 QUEUED > INCR product:1001:sold QUEUED

这里有一个容易忽略的点:MULTI 期间返回 QUEUED 只表示“命令已经进入队列”,并不代表命令一定能成功执行。命令真正被执行的时机是在 EXEC 发出之后。也就是说,队列里即使存在命令,你也无法提前拿到执行结果,所有结果都要等 EXEC 一次性返回。

另外,MULTI 不能嵌套。如果连接已经处于事务状态,再发送 MULTI,Redis 会直接报错。开发时如果用了连接池,一定要留意连接是否被复用,避免把上一个事务的状态带到下一条命令。

2.2 EXEC:把队列真正跑起来

EXEC 是触发事务执行的命令。它会把当前连接队列里排好的命令按顺序执行,然后把每条命令的结果打包成一个数组返回。

> EXEC 1) OK 2) (integer) 6

如果队列里没有任何命令就调用 EXEC,Redis 会返回空数组。另一个关键点是,一旦 EXEC 执行完毕,这个连接上的事务状态就自动结束了,不需要额外清理。

需要特别注意的是,EXEC 的原子性体现在“执行过程中不会被其他请求插队”,但不是说队列里所有命令都成功才叫成功。后面讲运行时错误时我会再展开,这里先记住一个结论:Redis 事务不会因为其中一条命令运行时报错,就把其他命令回滚掉。

2.3 DISCARD:随时反悔

如果 MULTI 之后、EXEC 之前,你发现命令队列有问题,或者不想执行了,可以直接发送 DISCARD。它会清空当前事务队列,并让连接恢复正常状态。

> MULTI OK > SET product:1001:stock 5 QUEUED > DISCARD OK

DISCARD 之后,之前排队的命令全部作废,并且如果之前使用了 WATCH,也会在 DISCARD 后解除监控。注意,DISCARD 只是在 EXEC 之前放弃,一旦 EXEC 已经发出,就没办法通过 DISCARD 反悔了。

从使用习惯上来说,我建议在事务队列构造失败、参数校验不过时优先考虑 DISCARD,而不是直接 EXEC 一个明知有问题的队列,这样可以避免无意义的执行。

2.4 WATCH/UNWATCH:乐观锁的正确用法

WATCH 是 Redis 事务里最容易被忽略、也最有用的命令。它解决的是“先读后写”场景下的条件竞争问题。典型例子是:先读取某个 key 的值,判断满足某个条件后,再在事务里修改它。如果读取之后、EXEC 之前,这个 key 被其他客户端改了,那么整个事务就应该作废。

WATCH 可以监控一个或多个 key。在事务提交前,如果被监控的 key 被其他客户端修改过,EXEC 会直接返回 nil,并拒绝执行整个事务队列。这个机制相当于给指定 key 加了一个乐观锁。

> WATCH product:1001:stock OK > GET product:1001:stock "5" # 这里假设另一个客户端突然执行了 INCR product:1001:stock > MULTI OK > DECR product:1001:stock QUEUED > EXEC (nil)

UNWATCH 则用于主动解除所有 WATCH 监控。它一般在你已经通过 GET 做判断、但最终决定不执行事务时使用,避免这个连接带着残留的监控进入下一次操作。

需要特别提醒:WATCH 必须在 MULTI 之前调用,这样才能在事务开始之前建立监控。如果在 MULTI 之后调用,它会被当成普通事务命令排队,等到 EXEC 时再执行,这时已经失去了“事务执行前检查修改”的意义。

3. 手把手跑通一个 Redis 事务

3.1 场景设计:库存扣减

这里我用一个非常常见的“下单减库存”场景来做演示。假设有一个 Redis key 叫product:1001:stock,当前库存是 10。用户下单需要做两件事:把库存减 1,然后写一条订单状态。

如果只是用 MULTI/EXEC 直接把 DECR 和 HSET 包起来,能做到不插队,但做不到“库存不足时不下单”。因为在 MULTI 里你没法根据判断结果临时决定要不要执行。所以,正确的姿势是用 WATCH 先监控库存 key,然后在事务开始前读取库存,由客户端判断是否够卖;够卖再拼装命令队列,不够卖就 UNWATCH 直接返回。

3.2 使用 WATCH + MULTI 的完整操作

初始数据:

> SET product:1001:stock 10 OK > SET product:1001:order 0 OK

客户端逻辑:

> WATCH product:1001:stock OK > GET product:1001:stock "10"

此时客户端在应用里做判断:10 > 0,可以下单。于是继续:

> MULTI OK > DECR product:1001:stock QUEUED > HSET order:1001 status "paid" QUEUED > EXEC 1) (integer) 9 2) (integer) 1

如果 WATCH 之后,另一个客户端抢先把product:1001:stock改成 9,那么同样的 EXEC 会返回:

> EXEC (nil)

这时候,DECR 和 HSET 都不会执行。客户端收到 nil 后,需要通过再次读取库存、再次判断、再次重试,来完成一次安全的下单流程。

3.3 核心细节:失败重试逻辑怎么写

WATCH 的事务在并发冲突时会静默返回 nil,所以应用层必须设计重试。重试的核心是循环一定次数,每次循环重新 WATCH、重新读取、重新判断,直到 EXEC 成功或超过最大重试次数。

拿 Python 里的 redis-py 来写大致伪代码:

MAX_RETRY = 3 def deduct_stock(r, product_key, order_key): for _ in range(MAX_RETRY): r.watch(product_key) stock = int(r.get(product_key) or 0) if stock <= 0: r.unwatch() return False pipe = r.pipeline(transaction=True) pipe.decr(product_key) pipe.hset(order_key, "status", "paid") try: pipe.execute() return True except WatchError: continue return False

这里有个细节很多人容易写错:重试时不能复用第一次创建的 pipeline,因为 WATCH 状态是绑定在连接上的,第二次重试必须重新 WATCH 并新建 pipeline,否则监控状态可能是旧的。我实际开发中踩过这个坑,看起来逻辑没问题,但并发一高就偶尔出现数据错误。

4. 常见问题与避坑指南

4.1 语法错误和运行时错误是两码事

Redis 事务里的错误分两个阶段,行为完全不同。阶段一是在命令入队时,如果 Redis 发现命令格式不对、参数个数不合法,会直接返回错误信息。此时队列里已经有一个非法命令,到 EXEC 时 Redis 会直接拒绝执行整个事务队列,返回错误。这类错误通常可以在 MULTI 阶段提前发现。

阶段二是命令在 EXEC 执行时发生错误,比如对 string 类型的 key 执行 LPUSH,Redis 会返回 WRONGTYPE 错误。和很多人想的不一样,这一条命令出错不会让后面的命令停止,也不会让前面已经执行的命令回滚。Redis 会把每条命令的执行结果依次放入返回数组,错误命令在数组里占一个 error 条目,后续命令照常执行。

> SET mykey "hello" OK > MULTI OK > LPUSH mykey item QUEUED > INCR cnt QUEUED > EXEC 1) (error) WRONGTYPE Operation against a key holding the wrong kind of value 2) (integer) 1

所以,如果事务里有多条命令,其中一条运行时出错,其他命令依然生效,数据可能处于“改了一半”的状态。设计业务时一定要对 key 类型和值类型做前置校验,或者考虑用 Lua 脚本在服务端做类型检查和条件判断。

4.2 事务和 Pipeline 别搞混

Pipeline 是把多个命令一次发给服务端,减少网络往返成本;事务则是把多个命令按原子方式串行执行。Pipeline 本身不提供任何隔离性,多个 Pipeline 里的命令依然可能被其他客户端的请求穿插执行。事务也不自带网络优化效果,MULTI 阶段每条命令仍然会收到一次 QUEUED 响应,网络往返不会减少。

开发中常见的组合方式是“Pipeline + 事务”:把 MULTI、多条命令、EXEC 一起打包发送,既能减少包数量,又能获得事务能力。但用的时候要小心,事务期间的所有中间响应(包括 QUEUED)也会一起回来,解析顺序一旦对不上,客户端就乱了。主流客户端库都有现成的 transaction 接口封装,建议优先用封装好的能力,而不是自己手工拼 MULTI/EXEC。

4.3 在 Cluster 模式下使用事务的限制

Redis Cluster 模式下,事务内操作的 key 必须落在同一个哈希槽上,否则 EXEC 会直接报错。要同时操作多个 key,可以利用哈希标签,比如把用户中心相关的 key 写成{user:123}:balance和{user:123}:lock,花括号内的内容参与哈希计算,这样可以强制让它们落到同一个槽上。

WATCH 同样受这个限制,被监控的 key 和事务内操作的 key 都应该在同一个槽内。如果你在 Cluster 环境里遇到“CROSSSLOT Keys in request don't hash to the same slot”错误,先检查 key 命名,别怀疑是事务命令写错了。

4.4 该用 MULTI/EXEC 还是 Lua 脚本

场景推荐方案原因
只需要无脑批量执行命令MULTI/EXEC简单直接,保留原子执行
需要在执行前判断 key 值做条件写WATCH + MULTI/EXEC乐观锁 + 客户端重试
需要复杂条件分支、循环、类型校验Lua 脚本(EVAL)逻辑在服务端执行,避免多次网络往返
需要出错后主动回滚前面操作Lua 脚本 + 手动补偿Redis 原生事务本身不回滚

一个很常见的认知是:Lua 脚本就是用来替代事务的。其实不完全对。Lua 脚本确实能解决“读改写”条件竞争问题,因为整个脚本在服务端原子运行,中途不会被其他命令插入。但如果脚本执行到一半发生错误,Redis 也不会自动撤销之前写入的数据。和原生事务一样,回滚要靠脚本内自己的补偿逻辑。

5. 几个常见应用场景和我的取舍心得

5.1 适合直接用 MULTI/EXEC 的场景

如果业务里只是需要把几个独立的写操作聚合在一起,不依赖读过的数据做判断,那 MULTI/EXEC 是我最喜欢的方案。比如批量更新多个计数器、同步写入缓存和标记位、一次性设置多个配置项,这类场景用事务能避免请求穿插,代码也很容易读。

这里有一个实测下来的习惯:尽量让事务内的命令数量保持在可控范围内。Redis 事务是单线程执行的,队列里命令太多,后面的所有客户端请求都会被阻塞。虽然没有硬性数量限制,但超过几十条命令时,你要认真想想是否真的应该放在同一个事务里。

5.2 真正需要 WATCH 的场景

需要“先读后写”且数据并发竞争明显的场景,WATCH 几乎是唯一的选择。举例来说:防止超卖、优惠券领取总数控制、秒杀库存预扣,这些场景都是先读当前值,判断可执行后再更新。WATCH 等于给这个判断过程加了一个版本校验,竞态发生时事务会失败,客户端重新判断即可。

不过要提醒一点:WATCH 冲突发生的概率高了,重试也就会频繁。如果某个热点 key 每秒被更新成千上万次,WATCH + 重试的代价可能会很大,或者出现活锁。这时候需要考虑把请求队列化、用分布式锁、或者让业务上做“最终一致性”而不是每次强一致。

5.3 我个人更推荐什么样的替代方案

在实际项目里,我见过很多人在不需要事务的地方强行加事务,最后既增加复杂度,又没换来回滚能力。Redis 很多原子命令本身就能解决一部分问题:INCR/DECR 自增自减是原子的,SET if NX 可以处理去重,HSETNX、LPUSH 等单命令操作也都有原子性。能用原子命令解决的,就不要套事务。

需要多命令强一致时,我更推荐优先考虑 Lua 脚本。原因很简单:Lua 脚本可以在服务端执行条件判断,不需要先 GET 回客户端判断再走一轮,逻辑上更紧凑;而且脚本整体原子运行,不会在判断和执行之间被其他请求钻空子。唯一要留意的是脚本要写得收敛,不能在里面做耗时循环,否则会阻塞整个 Redis 实例。

最后分享一个我踩过几次的坑:事务里尽量不要混用“读取某个 key 后决定写另一个 key”的操作。因为 MULTI 阶段不能动态决定下一步命令,只能把命令全部提前排好,一旦前面读到的数据影响后续命令逻辑,就必须借助 WATCH 和重试,而重试代码写不好很容易留下脏数据。看清 Redis 事务的边界,才能把它用在合适的位置。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 23:15:59

两级式光伏MMC并网系统交流故障穿越仿真建模与排坑指南

做光伏并网仿真的人应该都有个体会&#xff1a;光伏MMC并网系统&#xff08;两级式&#xff09;听起来是个很窄的题目&#xff0c;但真上手做交流故障穿越仿真时&#xff0c;却处处都是坑。电网对称/不对称故障怎么注入、故障期间控制策略怎么切换、MMC子模块电容电压会不会发散…

作者头像 李华
网站建设 2026/10/14 23:15:26

CMake多目录C++工程实战:从单目录到模块化构建的完整指南

1. 从单目录到多目录&#xff1a;为什么CMake工程迟早要迈过这道坎刚开始写C的时候&#xff0c;几乎所有人都是从单目录起步的。一个main.cpp&#xff0c;一个CMakeLists.txt&#xff0c;三五行配置就能跑起来&#xff0c;编译、链接、运行一气呵成。这种体验很爽&#xff0c;但…

作者头像 李华
网站建设 2026/10/14 23:15:13

JMeter脚本调试全攻略:从结果树到关联提取与参数化

做接口和性能压测这些年&#xff0c;我见过太多人在 JMeter 脚本上反复折腾&#xff1a;脚本明明是按需求写的&#xff0c;跑起来响应全红&#xff1b;业务链路明明是通的&#xff0c;断言却永远失败&#xff1b;本地一两个线程跑得好好的&#xff0c;压测机上一上并发就各种超…

作者头像 李华
网站建设 2026/10/14 23:14:40

065、扫描请求与扫描响应的交互时序异常定位

上个月帮一个团队排查产线测试工装的偶发故障&#xff0c;现象很怪&#xff1a;同一套测试脚本&#xff0c;跑扫描请求的时候&#xff0c;十次里有那么一两次&#xff0c;返回的扫描响应里会缺几个目标&#xff0c;或者目标的位置坐标明显偏移。重跑一遍又正常了。测试操作员已…

作者头像 李华
网站建设 2026/10/14 23:14:29

AI少儿英语App开发实战:语音评测与自适应学习

1. 项目整体设计&#xff1a;AI少儿英语App到底该怎么做做AI少儿英语App这个项目&#xff0c;我一开始的直觉是“把大模型塞进去就行了”&#xff0c;但真正动手之后发现&#xff0c;这事远没有想象中那么简单。市面上的AI英语学习产品一大堆&#xff0c;有的主打口语陪练&…

作者头像 李华
网站建设 2026/10/14 23:14:24

GeoServer JMS集群部署实战:ActiveMQ消息同步与高可用架构详解

聊聊GeoServer的JMS集群部署。这个方案我在生产环境里前前后后折腾了小半年&#xff0c;从最初两台机器照着网上的碎片教程硬凑&#xff0c;到现在基本上小半天能搭出一套可用的多节点环境&#xff0c;中间踩过的坑值得写下来。GeoServer是开源GIS服务器里用得最多的那一个&…

作者头像 李华