文章目录
- [Redis 事务的命令组合](#Redis 事务的命令组合)
- [用 ACID 标准重新审视](#用 ACID 标准重新审视)
- WATCH:实现乐观锁
- [事务 vs Lua 脚本](#事务 vs Lua 脚本)
- 事务的常见误区
-
- [误区 1:以为 EXEC 失败会回滚](#误区 1:以为 EXEC 失败会回滚)
- [误区 2:以为 WATCH 是悲观锁](#误区 2:以为 WATCH 是悲观锁)
- [误区 3:忘了 EXEC 之后还可能 nil](#误区 3:忘了 EXEC 之后还可能 nil)
- [误区 4:事务里执行慢命令](#误区 4:事务里执行慢命令)
- 实践建议

数据库的事务都讲 ACID------原子性、一致性、隔离性、持久性。这四个性质是关系型数据库的根基。Redis 也提供了事务机制,但要回答"Redis 能不能做事务",必须把这四个性质拆开来看。结论是:Redis 的事务和传统数据库事务有本质差异,更像是"批量原子执行"。
Redis 事务的命令组合
Redis 的事务由四个命令组成:
- MULTI:开启事务,后续命令进入队列。
- EXEC:提交事务,依次执行队列里的所有命令。
- DISCARD:放弃事务,清空队列。
- WATCH:监视一个或多个 key,事务期间被改就放弃执行。
典型用法:
java
MULTI
SET account:A 100
SET account:B 200
INCRBY account:A -50
INCRBY account:B 50
EXEC
MULTI 之后到 EXEC 之前的所有命令都进入队列,不立即执行。EXEC 一旦发出,队列里的命令依次执行,期间不会被其他客户端打断。
用 ACID 标准重新审视
原子性(A):部分支持
传统事务的原子性是"全部成功或全部回滚"。Redis 的情况复杂一些:
情况 1 :命令入队时就有错(比如命令名拼错)。Redis 5.0+ 会拒绝整个事务,EXEC 返回错误,所有命令都不执行。这种情况下原子性是有的。
情况 2 :入队时语法没错,但执行时出错(比如对 String 用 LPUSH)。Redis 会继续执行后面的命令,前面成功的命令不会回滚。这种情况下原子性是缺失的。
java
MULTI
SET key1 "hello"
LPUSH key1 "world" # 类型错误,但 EXEC 时才暴露
SET key2 "ok"
EXEC
# 结果:key1 = "hello",key2 = "ok",没有回滚
为什么不支持回滚?Antirez 的解释是:Redis 事务里出错的几乎都是"程序员的 bug",回滚也救不了你;而且不支持回滚让 Redis 实现更简单、性能更好。
一致性(C):依赖原子性
一致性指事务执行前后数据库都处于合法状态。在 Redis 里:
- 如果事务原子执行成功,最终状态自然合法。
- 如果中间命令失败但后面继续执行,就可能进入"半完成"状态,违反一致性。
所以 Redis 事务的一致性强弱完全取决于上面说的原子性场景。
隔离性(I):完全支持
EXEC 开始执行后,整个队列里的命令是串行运行的,期间不会有其他客户端的命令插入。这是单线程模型给的天然隔离。
但要注意:MULTI 到 EXEC 之间的"入队期",其他客户端是可以正常操作的。如果你需要"开启事务时数据状态不变",必须配合 WATCH。
持久性(D):取决于持久化配置
事务执行结果是否持久化,取决于 Redis 的持久化策略:
- 关闭持久化:完全不持久。
- AOF + appendfsync everysec:最多丢 1 秒。
- AOF + appendfsync always:理论上不丢。
也就是说,Redis 事务本身不提供持久性保证,要靠 AOF 配合。
WATCH:实现乐观锁
WATCH 是 Redis 事务的精髓,让事务支持"基于条件的提交"。
典型场景:账户扣款,需要保证扣款时余额不变。
java
WATCH account:A
balance = GET account:A
if balance < 100:
UNWATCH
return "余额不足"
MULTI
DECRBY account:A 100
EXEC # 如果在 WATCH 之后 account:A 被改过,EXEC 返回 nil
机制:WATCH 后,Redis 会跟踪这些 key。EXEC 执行时,如果发现任何被监视的 key 被其他客户端改过,事务就整体放弃,返回 nil。
这是典型的乐观锁:先假设没人会改,真到提交时再检查。客户端发现失败要自行重试。
事务 vs Lua 脚本
Lua 脚本能实现的事情,Redis 事务也能做。但两者各有取舍:
| 维度 | MULTI/EXEC | Lua 脚本 |
|---|---|---|
| 中间结果可见 | 不可见(只能在 EXEC 后看结果) | 可见(脚本里可以基于中间结果判断) |
| 条件分支 | 弱(只能 WATCH) | 强(脚本里随便写) |
| 错误处理 | 不支持回滚 | 可以提前返回 |
| 网络开销 | 多次往返 | 一次调用 |
| 可读性 | 命令直观 | 需要懂 Lua |
实际选择:
- 简单批量操作:用 MULTI/EXEC,可读性好。
- 复杂逻辑(需要中间判断、循环):用 Lua。
- 需要严格条件提交:MULTI + WATCH 或者 Lua。
事务的常见误区
误区 1:以为 EXEC 失败会回滚
事务里命令执行失败不会回滚。如果业务依赖回滚,必须自己写补偿逻辑。
误区 2:以为 WATCH 是悲观锁
WATCH 是乐观锁,发现冲突就放弃,不阻塞其他客户端。如果竞争激烈,重试次数会很多。
误区 3:忘了 EXEC 之后还可能 nil
被 WATCH 的 key 被改了,EXEC 返回 nil 而不是错误。客户端要主动检查这个返回值。
误区 4:事务里执行慢命令
EXEC 期间会阻塞整个 Redis。事务里的命令必须都是快速的,避免长时间阻塞。
实践建议
- 明白 Redis 事务的真实保证:原子性弱、隔离性强、持久性靠 AOF。
- 复杂逻辑优先用 Lua:Lua 能做的事情比事务更多,且只需一次网络往返。
- 乐观锁场景用 WATCH:余额检查、库存扣减等"先看再改"的场景。
- 事务里不放慢命令:避免 EXEC 期间阻塞 Redis。
- 业务上做好幂等设计:因为 Redis 事务不能回滚,业务层要能容忍重试不重复扣款。
Redis 的事务机制提供了最基本的"批量原子执行"能力,但远不能和数据库事务相提并论。理解它的边界------什么能保证、什么不能------才能在合适的场景用对它。需要更复杂的事务语义时,要么换用 Lua 脚本,要么直接用关系型数据库。
