news 2026/10/16 3:06:19

Redis 事务为什么没回滚:MULTI 的 3 个反直觉行为

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 事务为什么没回滚:MULTI 的 3 个反直觉行为

我不会起名字322· 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • Redis 事务为什么没回滚:MULTI 的 3 个反直觉行为
    • 一、先搞清根本区别:Redis 事务**不支持回滚**
    • 二、行为 1:语法错误 → 整个事务不执行
    • 三、行为 2:运行时错误 → 只跳过那一条
    • 四、行为 3:WATCH 乐观锁的失效时机
    • 五、三种原子性方案对照表(可带走物)
    • 六、什么时候不该用 MULTI
    • 七、总结

Redis 事务为什么没回滚:MULTI 的 3 个反直觉行为

“我用了 MULTI,里面一条命令失败了,前面几条怎么还生效了?”

这是一位做订单系统的同事问的。他的思路很正常——数据库事务里一条失败就整体回滚,Redis 的 MULTI 应该也一样。但真不一样。

这篇讲清 Redis 事务和数据库事务的本质区别,拆 3 个反直觉行为,并给出"到底该用什么"的选型表。

一、先搞清根本区别:Redis 事务不支持回滚

这是最核心的一点,也是所有困惑的源头:

特性数据库事务Redis 事务
原子性(Atomicity)全部成功或全部失败部分成功(不保证)
隔离性(Isolation)支持不同隔离级别命令间不会被其他客户端插入(EXEC 期间)
一致性(Consistency)支持弱
持久性(Durability)取决于配置取决于 AOF 配置
回滚支持❌ 不支持

为什么 Redis 不做回滚?官方理由:回滚需要额外的日志和状态管理,会显著增加复杂度,而 Redis 认为语法错误可以在入队时就发现(见下文),运行时错误是"程序 bug"而非"应该回滚的业务场景"。

二、行为 1:语法错误 → 整个事务不执行

如果某条命令在入队时就被发现有语法错误(比如命令名拼错、参数个数不对),Redis 会:

  1. 该命令入队失败;
  2. EXEC时整个事务被拒绝执行(返回错误);
  3. 一条都不会执行。
127.0.0.1:6379>MULTI OK127.0.0.1:6379(TX)>SET k1 v1 QUEUED127.0.0.1:6379(TX)>SET k2# ❌ 参数个数不对(缺 value)(error)ERR wrong number of argumentsfor'set'command127.0.0.1:6379(TX)>INCR k1 QUEUED127.0.0.1:6379(TX)>EXEC(error)EXECABORT Transaction discarded because of previous errors.

注意:EXECABORT—— 整个事务被丢弃,k1也没被设置。

这是唯一"看起来像回滚"的情况,但本质是"入队阶段就发现错误,所以根本没执行",不是执行后回滚。

判据:入队时返回QUEUED才是成功;返回(error)就是入队失败。

三、行为 2:运行时错误 → 只跳过那一条

真正的"反直觉"在这里。如果命令入队成功了,但执行时才出错(比如对 string 做LPUSH、INCR一个非数字值),那么:

  • 只有出错的那条失败;
  • 其他命令全部生效;
  • 没有回滚。
127.0.0.1:6379>SET str"hello"OK127.0.0.1:6379>MULTI OK127.0.0.1:6379(TX)>SET k1 v1 QUEUED127.0.0.1:6379(TX)>INCR str# str 是 "hello",不是数字QUEUED# 入队成功!(运行时才会错)127.0.0.1:6379(TX)>SET k2 v2 QUEUED127.0.0.1:6379(TX)>EXEC1)OK# k1 设置成功2)(error)ERR value is not an integer or out of range# INCR 失败3)OK# k2 设置成功

结果:k1和k2都生效了,只有INCR str失败。这就是"没回滚"的现场。

关键判据:入队时不检查类型,所以类型错误只能在执行时暴露。这类错误无法在事务开始前发现。

四、行为 3:WATCH 乐观锁的失效时机

WATCH是 Redis 提供的"乐观锁",但它的语义常被误解:

# 客户端 AWATCH balance GET balance# 读到 100MULTI DECRBY balance30# 想扣 30# 此时客户端 B 修改了 balanceEXEC# 返回 nil(事务被放弃),因为 balance 被改过

规则:WATCH监视的 key,如果在EXEC之前被任何客户端修改,则EXEC返回nil,整个事务不执行。

反直觉点:

误解实际
WATCH 能锁住 key 不让别人改❌ 不锁,只是"如果被改过就放弃"
WATCH 后的读一定是"当前值"❌ 读和 EXEC 之间仍可能被改
事务被放弃会自动重试❌ 需要应用层自己重试(循环)

WATCH 的正确用法(应用层重试):

functransfer(ctx context.Context,rdb*redis.Client,from,tostring,amountint64)error{forattempt:=0;attempt<3;attempt++{// 必须自己重试!err:=rdb.Watch(ctx,func(tx*redis.Tx)error{bal,err:=tx.Get(ctx,from).Int64()iferr!=nil{returnerr}ifbal<amount{returnfmt.Errorf("余额不足")}_,err=tx.TxPipelined(ctx,func(pipe redis.Pipeliner)error{pipe.DecrBy(ctx,from,amount)pipe.IncrBy(ctx,to,amount)returnnil})returnerr},from)iferr==redis.TxFailedErr{// 被其他客户端改动,重试continue}returnerr}returnfmt.Errorf("重试 3 次仍失败")}

关键:redis.TxFailedErr是"WATCH 失效"的信号,必须重试,否则就是静默失败。

五、三种原子性方案对照表(可带走物)

方案原子性能否读后写复杂度适用场景
单命令(INCR/SETNX)✅ 强❌ 只能单步最低计数器、简单锁
MULTI/EXEC⚠️ 部分(无回滚)❌ 不能依赖前一条结果低批量独立写
WATCH + MULTI⚠️ 乐观锁(需重试)✅ 可以中需要读后写的简单场景
Lua 脚本✅ 强(单线程执行)✅ 可以中推荐的复杂原子操作

Lua 是最终答案——它在服务端单线程执行,整段脚本要么跑完要么不跑,且能读后写:

-- deduct_stock.lua:库存扣减,原子localstock=tonumber(redis.call('GET',KEYS[1])or'0')localqty=tonumber(ARGV[1])ifstock<qtythenreturn-1-- 库存不足endredis.call('DECRBY',KEYS[1],qty)returnstock-qty-- 返回扣减后的库存
vardeductStock=redis.NewScript(` local stock = tonumber(redis.call('GET', KEYS[1]) or '0') local qty = tonumber(ARGV[1]) if stock < qty then return -1 end redis.call('DECRBY', KEYS[1], qty) return stock - qty `)remaining,err:=deductStock.Run(ctx,rdb,[]string{"stock:1001"},2).Int64()ifremaining==-1{// 库存不足,业务上拒绝}

为什么 Lua 优于 WATCH:WATCH 需要应用层重试(高并发下重试率高),Lua 在服务端一次执行,没有重试开销。

六、什么时候不该用 MULTI

场景别用 MULTI 的原因
命令之间有依赖MULTI 里不能读前一条的结果
需要失败回滚Redis 事务不支持回滚
高并发扣减/转账用 WATCH 会大量重试,用 Lua 更好
只是想批量提速用pipeline(不开 MULTI,见另一篇)

七、总结

  1. Redis 事务不支持回滚—— 这是和数据库事务的根本区别;
  2. 3 个反直觉行为:语法错误整体不执行(EXECABORT)、运行时错误只跳过那一条(其他照常生效)、WATCH 失效返回 nil 且必须应用层重试;
  3. 入队不检查类型,所以类型错误只能在执行时暴露,无法预防;
  4. 需要原子 + 读后写:首选 Lua 脚本;需要批量提速:用 pipeline(不要 MULTI);
  5. 用 WATCH 就必须写重试循环,否则等于静默失败。

留个问题给你:你们扣库存是用WATCH还是 Lua 脚本?有没有踩过"以为 MULTI 会回滚"的坑?

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

LangChain 上线部署:把链和 Agent 变成能对外服务的 API

个人主页&#xff1a;> for_ever_love__ <&#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 大模型开发从0到1 < 其他栏目: > iOS项目总结大全 < 其他栏目: > 我想学python了 < 其他栏目: > iOS UI < 文章目录LangChain 上线…

作者头像 李华
网站建设 2026/10/16 3:05:09

如何安装任意版本docker

Docker 是一个基于容器技术的开源平台&#xff0c;旨在通过容器化简化应用的开发、部署和运行。其核心理念是“Build once, Run anywhere”&#xff08;构建一次&#xff0c;到处运行&#xff09;&#xff0c;将应用程序及其依赖项打包到轻量级、可移植的容器中&#xff0c;确保…

作者头像 李华
网站建设 2026/10/16 3:04:01

如何通过AI快速制作爆款视频(干货)

很多人还在熬夜剪片子&#xff0c;但真正决定一条视频能不能爆的环节&#xff0c;已经变了。 AI 把「制作」这个环节彻底平权了&#xff1a;脚本有 AI 写&#xff0c;形象有数字人演&#xff0c;剪辑有 AI 剪。制作流程本身&#xff0c;已经不再是拉开差距的地方。 真正决定视频…

作者头像 李华
网站建设 2026/10/16 3:02:13

存储器层次结构

前言 下面是《深入理解计算机系统》第六章节的一段话 对系统的研究中&#xff0c;我们依赖于一个简单的计算机系统模型&#xff0c; CPU 执行指令&#xff0c;而存储器系统为 CPU 存放指令和数据 。 在简单模型中&#xff0c;存储器系统是一个线性 的字节数组&#xff0c;而 CP…

作者头像 李华
网站建设 2026/10/16 3:02:06

自回归模型核心原理与实战:从链式法则到Transformer解码器

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华