写在前面的话
作为一个长期和关系型数据库(RDBMS)打交道的开发者,初次查阅 Redis 文档时,看到 MULTI、EXEC、DISCARD 这些指令,心中难免涌起一股由于熟悉而带来的安全感。
我们的大脑会自动建立映射:MULTI 就是 BEGIN,EXEC 就是 COMMIT,DISCARD 就是 ROLLBACK。这套组合拳打下来,所有的业务逻辑似乎都应该具备了“不成功便成仁”的原子性保障。
但这恰恰是 Redis 给我上的第一课:相似的命名背后,往往藏着截然不同的灵魂。 当你把 MySQL 的事务观生搬硬套到 Redis 身上时,错付就已经开始了。
这篇文章将带你剥开 Redis 事务的外衣,从“原子性”的定义偏差说起,聊聊为什么在现代开发中,我们越来越倾向于用 Lua 脚本来替代它。
一、先把误会解开:Redis 事务不是 ACID
在关系型数据库的世界里,“事务”二字重若千钧,它几乎等同于 ACID(原子性、一致性、隔离性、持久性)。我们习惯了“要么全有,要么全无”的安全感。
而在 Redis 的世界里,MULTI 和 EXEC 更像是一个批处理信号:
把一堆命令先放进队列里排队,等到 EXEC 时,一次性、按顺序地执行它们。
这里有一个巨大的认知偏差。当我们谈论 Redis 的“原子性”时,Redis 指的其实是 隔离性(Isolation),而不是 回滚(Rollback)。
- 它保证的是:我执行这段命令的时候,别人不能插队(独占执行)。
- 它不保证的是:如果我执行到一半报错了,我会帮你把前面的操作撤销(失败回滚)。
为了更直观地理解,我们可以对比一下 Redis 事务和标准 ACID 事务的区别:
| 特性 | 关系型数据库 (MySQL) | Redis 事务 | 差异解读 |
|---|
| 原子性 (Atomicity) | All or Nothing 失败即回滚,如同未发生过 | All or Partial 没得商量,错了就错了,剩下的接着干 | Redis 不支持 Rollback,部分成功是常态 |
| 一致性 (Consistency) | 强一致性 约束必须满足 | 弱一致性 依赖业务代码保障 | Redis 不会校验业务约束(如外键、非空等) |
| 隔离性 (Isolation) | 有多种隔离级别 (RC/RR/Serializable) | 串行化执行 执行期间不可被打断 | 得益于单线程模型,EXEC 期间天然隔离 |
| 持久性 (Durability) | WAL 日志保障 掉电不丢失 | 取决于 AOF/RDB 配置 | 默认配置下通常有数据丢失风险 |
一句话总结:
Redis 事务是“命令队列 + 独占执行”,绝不是“失败回滚 + 强一致”。
二、残酷的真相:它真的不包回滚
为了把这个概念刻进 DNA,我们看两种真实的错误场景。
1. 入队时的“低级错误”(全员连坐)
如果你在命令入队阶段就犯了语法错误(比如参数写少了),Redis 还是讲道理的,它会直接拒绝整个事务。
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> SET key2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
这时候,所有命令都不会执行。这符合我们对“事务”的预期。
2. 执行时的“运行时错误”(虽死犹进)
这才是真正的坑。假设语法没问题,但在执行期间,某条命令因为数据类型不匹配报错了:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET user:A:points 100
QUEUED
127.0.0.1:6379> LPUSH user:A:points "error_data"
QUEUED
127.0.0.1:6379> INCR user:A:points
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value <--- 报错!
3) (integer) 101 <--- 依然成功了!
目瞪口呆了吗?
第二条命令报错了,但第三条命令依然欢快地执行了。数据出现了中间态:即所谓的“不一致”。
Redis 官方对此的解释非常“直男”:
“只有语法错误才会被拦截,运行时错误属于程序员的逻辑 Bug(比如把 String 当 List 用)。数据库不应该为了程序员的 Bug 买单,去搞复杂的回滚机制。”
三、进阶之路:从原生批量到 Lua 脚本
💡 预备知识:RTT 是性能杀手
一个 Redis 命令的执行可以简化为 4 步:发送命令 → 命令排队 → 命令执行 → 返回结果。
其中,第 1 步和第 4 步的时间之和称为 RTT (往返时间)。如果我有 100 个命令,一个个发就需要 100 次 RTT,大部分时间都浪费在网络传输上。
批量操作的核心意义,就是把 100 次 RTT 压缩成 1 次。
既然 MULTI/EXEC 这么“头铁”,那我们在实际开发中到底该怎么选?我们可以把 Redis 的批量操作能力分为几个段位。
Lv1. 原生批量命令 (MSET / MGET)
这是最简单、最快的方式。
Lv2. 管道 (Pipeline)
当你需要批量执行几十个不同的命令,且不需要它们之间有逻辑依赖时,Pipeline 是首选。
- 特点:唯快不破。它把几十个命令打包,一次网络请求(RTT)发给服务器,服务器执行完再一次性返回。
- 形象理解:下 100 个单 -> 一次性收 100 个快递 (1 次 RTT)。
- 与事务的区别:
- 非原子性:Pipeline 只是打包发送,Redis 可能会在处理 Pipeline 中间穿插执行其他客户端的命令(交错执行)。
- 效率更高:不需要像事务那样每个命令都发一次,只需要发送一次。
Lv3. 事务 (MULTI / EXEC)
比 Pipeline 多了一层保障:独占执行。
- 特点:原子操作(隔离性)。
- 两个不同的事务不会同时运行。在
EXEC 执行期间,Redis 会“以此为尊”,保证没有其他客户端能插队。
- 缺点:
- RTT 开销大:事务中 每个命令都需要单独发送 到服务端入队,请求次数并没有减少。
- 不支持回滚,不支持在事务中间做逻辑判断。
Lv3.1 事务 + WATCH (乐观锁)
单纯的 MULTI/EXEC 往往比较鸡肋,因为它无法感知中间状态。但这套机制唯一的“王牌”组合是配合 WATCH 命令,实现乐观锁 (CAS)。
Lv4. 最终兵器 —— Lua 脚本
从 Redis 2.6 开始,Lua 脚本成为了解决复杂原子性问题的核心方案,它完美替代了 WATCH 事务。
为什么它比事务强?
- 逻辑原子性:一段 Lua 脚本被视作一条命令。Redis 保证脚本执行期间,不会有任何其他脚本或命令插入。
- 效率更高:不需要像
WATCH 那样反复重试。脚本在服务器端执行,只有一次 RTT。
示例:安全的“先查后改”
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
⚠️ 必须警惕的缺陷:Lua 也不回滚!
虽然 Lua 脚本被称为“原子操作”,但请注意:它的原子性依然指的是不被打扰,而不是失败回滚。
如果 Lua 脚本运行到中途出错(比如调用了不存在的命令,或显式报错退出),脚本会停止执行,但之前已经执行过的写操作,是不会被撤销的!
这意味着,即使是 Lua,也不能给你带来 RDBMS 那种“回滚一切”的安全感。你依然需要在代码层面保证逻辑的严密性。
四、总结:选型决策表
为了让你在实际业务中不再纠结,我整理了一份简单的决策表:
| 需求场景 | 推荐方案 | 核心理由 |
|---|
| 简单批量读写 (KV) | MSET / MGET | 原生命令,最快,最省心。 |
| 大量离散命令 (无关联) | Pipeline | 网络开销最低,吞吐量最高。 |
| 需要 CAS (低并发) | WATCH + MULTI | 事务唯一的用武之地。 适合低频竞争,实现简单。 |
| 复杂逻辑 / 高并发 | Lua 脚本 | 行业标准。 避免了 CAS 自旋的性能开销,原子性强。 |
| 即使报错也要回滚 | MySQL / RDBMS | 别为难 Redis。 它没有 Undo Log,做不到真正的回滚。 |
写在最后
回头看,Redis 事务这套机制,就像是一个“如果不仔细读说明书一定会用错”的半成品。
但正是这个“半成品”,折射出了 Redis 最底层的价值观:为了性能,可以牺牲一切“看起来很美”的抽象。它拒绝了沉重的 Undo Log,拒绝了复杂的隔离级别,只留下了一个最简单的“排队执行”逻辑。
所以,当我们下次再写下 MULTI 的时候,心里要清楚:
- 如果只是为了快,Pipeline 才是那个不讲武德的“加速器”。
- 如果只是为了防插队,Transaction 够用了,但在高并发下,它脆弱得像个易碎品。
- 如果要处理真正的复杂逻辑,请毫不犹豫地拥抱 Lua —— 虽然它也不会回滚,但至少在“执行原子性”上,它是我们手里最稳的那张牌。