07 - 事务协议
事务协议
7.1 Redis 事务概述
Redis 事务是一组命令的打包执行。与关系数据库事务不同,Redis 事务不支持回滚——它提供的是"原子性"(所有命令要么全部执行,要么全部不执行),而非"事务性"(ACID)。
事务生命周期
普通模式 → MULTI → 命令排队 → EXEC/DISCARD → 普通模式
| 阶段 | 行为 |
|---|---|
| MULTI | 进入事务模式,后续命令排队而非立即执行 |
| 命令排队 | 命令被缓存,返回 QUEUED |
| EXEC | 执行所有排队的命令,返回结果数组 |
| DISCARD | 取消事务,丢弃所有排队的命令 |
| WATCH | 在事务开始前监视 key,实现乐观锁 |
7.2 MULTI 命令
命令格式
*1\r\n$5\r\nMULTI\r\n
响应
+QUEUED\r\n
MULTI 之后,服务器进入事务模式。后续命令不再立即执行,而是排队等待。
事务模式下的命令处理
$ telnet 127.0.0.1 6379
*1\r\n$5\r\nMULTI\r\n
+QUEUED
*3\r\n$3\r\nSET\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n
+QUEUED
*3\r\n$3\r\nSET\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n
+QUEUED
*2\r\n$3\r\nGET\r\n$4\r\nkey1\r\n
+QUEUED
每个命令返回 +QUEUED,表示已排队。
7.3 EXEC 命令
命令格式
*1\r\n$4\r\nEXEC\r\n
响应格式
EXEC 返回一个数组,包含所有排队命令的执行结果:
*3\r\n
+OK\r\n ← SET key1 value1 的结果
+OK\r\n ← SET key2 value2 的结果
$6\r\nvalue1\r\n ← GET key1 的结果
完整事务示例
→ MULTI
← +OK
→ SET account:a 100
← +QUEUED
→ SET account:b 200
← +QUEUED
→ DECRBY account:a 30
← +QUEUED
→ INCRBY account:b 30
← +QUEUED
→ EXEC
← *4
← +OK
← +OK
← :70
← :230
7.4 DISCARD 命令
命令格式
*1\r\n$7\r\nDISCARD\r\n
响应
+OK\r\n
DISCARD 取消当前事务,丢弃所有排队的命令:
→ MULTI
← +OK
→ SET key1 value1
← +QUEUED
→ SET key2 value2
← +QUEUED
→ DISCARD
← +OK
→ GET key1
← ... (key1 未被修改)
7.5 事务中的错误处理
Redis 事务中有两种类型的错误:
类型一:命令排队阶段的错误
在 MULTI 和 EXEC 之间,如果命令格式错误或不存在,Redis 会在排队阶段就报错:
→ MULTI
← +OK
→ SET key1 value1
← +QUEUED
→ INVALID_COMMAND
← -ERR unknown command 'INVALID_COMMAND'
→ SET key2 value2
← +QUEUED
→ EXEC
← -EXECABORT Transaction discarded because of previous errors.
行为:EXEC 返回错误,整个事务被丢弃(不执行任何命令)。
类型二:命令执行阶段的错误
如果命令在排队阶段成功,但在执行阶段失败(如类型错误),其他命令仍然会执行:
→ MULTI
← +OK
→ SET mykey "hello"
← +QUEUED
→ LLEN mykey ← 排队成功,但执行时 mykey 不是列表
← +QUEUED
→ SET anotherkey "world"
← +QUEUED
→ EXEC
← *3
← +OK ← SET 成功
← -WRONGTYPE Operation against a key ← LLEN 失败
← +OK ← SET 成功(仍然执行!)
行为:其他命令不受影响,继续执行。Redis 事务不支持回滚。
为什么不支持回滚?
Redis 设计者认为:
- 回滚增加了复杂性,但 Redis 命令很少失败
- 命令失败通常是编程错误(如类型不匹配),而非运行时错误
- 不支持回滚让 Redis 更简单、更快
7.6 WATCH 命令(乐观锁)
什么是乐观锁
乐观锁假设"冲突很少发生",在提交事务时检查数据是否被修改:
┌─────────────────────────────────────────────┐
│ 1. WATCH key │ ← 监视 key
│ 2. 读取 key 的值 │ ← 获取当前状态
│ 3. 计算新值 │ ← 本地计算
│ 4. MULTI │ ← 开始事务
│ 5. SET key new_value │ ← 排队写入
│ 6. EXEC │ ← 执行事务
│ └─ 如果 key 在 WATCH 后被修改 → 返回 NULL │ ← 检查失败
│ └─ 如果 key 未被修改 → 正常执行 │ ← 检查成功
└─────────────────────────────────────────────┘
WATCH 命令格式
*2\r\n$5\r\nWATCH\r\n$3\r\nkey\r\n
响应
+OK\r\n
EXEC 的特殊返回值
当 WATCH 检测到 key 被修改时,EXEC 返回 NULL 而非数组:
# 正常执行
→ EXEC
← *2 ← 数组,包含两个结果
← +OK
← +OK
# 被取消(WATCH 检测到冲突)
→ EXEC
*-1 ← NULL,事务未执行
7.7 WATCH 实战:实现转账
import redis
def transfer(r: redis.Redis, from_key: str, to_key: str, amount: int):
"""
使用 WATCH 实现乐观锁转账
原子性保证:如果在事务执行前有任何 key 被修改,事务自动重试
"""
max_retries = 100
for attempt in range(max_retries):
try:
# 1. 监视相关 key
r.watch(from_key, to_key)
# 2. 读取当前余额
from_balance = int(r.get(from_key) or 0)
to_balance = int(r.get(to_key) or 0)
# 3. 检查余额是否充足
if from_balance < amount:
r.unwatch()
raise ValueError(f"Insufficient balance: {from_balance} < {amount}")
# 4. 开始事务
pipe = r.pipeline()
pipe.multi()
pipe.decrby(from_key, amount)
pipe.incrby(to_key, amount)
# 5. 执行事务
results = pipe.execute()
# 如果 execute() 没有抛出 WatchError,说明事务成功
print(f"Transfer successful: {amount} from {from_key} to {to_key}")
return True
except redis.exceptions.WatchError:
# key 被其他客户端修改,重试
print(f"Attempt {attempt + 1}: Conflict detected, retrying...")
continue
raise RuntimeError(f"Transfer failed after {max_retries} attempts")
# 使用
r = redis.Redis(host="127.0.0.1", port=6379)
r.set("account:a", 1000)
r.set("account:b", 500)
transfer(r, "account:a", "account:b", 100)
print(f"account:a = {r.get('account:a')}") # 900
print(f"account:b = {r.get('account:b')}") # 600
7.8 WATCH 的协议细节
WATCH 多个 key
*3\r\n$5\r\nWATCH\r\n$5\r\nkey1\r\n$5\r\nkey2\r\n
WATCH 可以监视多个 key,任意一个 key 被修改都会导致 EXEC 失败。
UNWATCH
*1\r\n$7\r\nUNWATCH\r\n
UNWATCH 取消所有 WATCH 监视。DISCARD 也会自动取消监视。
WATCH 的生命周期
| 事件 | WATCH 状态 |
|---|---|
| WATCH key | 开始监视 |
| EXEC 成功 | 自动取消监视 |
| EXEC 失败(冲突) | 自动取消监视 |
| DISCARD | 自动取消监视 |
| UNWATCH | 手动取消监视 |
| 连接断开 | 自动取消监视 |
7.9 事务中的命令限制
在 MULTI 和 EXEC 之间,以下命令有特殊行为:
不允许的命令
| 命令 | 原因 |
|---|---|
MULTI | 不能嵌套事务 |
WATCH | WATCH 必须在 MULTI 之前 |
会触发展开的命令
某些命令在事务中会立即执行(不会排队):
| 命令 | 行为 |
|---|---|
EXEC | 执行事务并退出事务模式 |
DISCARD | 取消事务并退出事务模式 |
AUTH | 立即认证(Redis 6.0+) |
HELLO | 立即执行 |
RESET | Redis 7.0+,重置连接 |
会进入 Pub/Sub 的命令
在事务中执行 SUBSCRIBE 会导致连接进入 Pub/Sub 模式,这通常不是期望的行为。
7.10 完整事务协议流程
Client Server
│ │
│──── WATCH mykey ─────────────────→│ 注册监视
│←─── +OK ─────────────────────────│
│ │
│──── GET mykey ──────────────────→│ 读取当前值
│←─── $5\r\nhello ────────────────│
│ │
│──── MULTI ──────────────────────→│ 进入事务
│←─── +OK ─────────────────────────│
│ │
│──── SET mykey world ────────────→│ 排队
│←─── +QUEUED ─────────────────────│
│ │
│──── EXEC ───────────────────────→│ 执行
│←─── *1\r\n+OK ─────────────────│ 成功(mykey 未被修改)
────── 或者(mykey 被其他客户端修改)──────
│──── EXEC ───────────────────────→│ 执行
│←─── *-1\r\n ────────────────────│ 失败(WATCH 冲突)
7.11 WATCH 冲突检测的实现原理
Redis 使用一种高效的机制来检测 WATCH 冲突:
标志位机制
// Redis 源码中的简化逻辑
typedef struct redisDb {
dict *dict; // key-value 存储
dict *watched_keys; // key → client 列表的映射
// ...
} redisDb;
WATCH key时:将当前连接加入watched_keys[key]的列表- 任何修改 key 的命令(SET、DEL 等)执行后:检查
watched_keys[key],标记相关客户端的CLIENT_DIRTY_CAS标志 EXEC时:检查CLIENT_DIRTY_CAS标志,如果被设置则返回 NULL
性能特点
| 操作 | 时间复杂度 |
|---|---|
| WATCH | O(1) per key |
| 冲突检测(写入时) | O(n) per key(n 为监视该 key 的客户端数) |
| EXEC 检查 | O(1)(检查标志位) |
7.12 事务 vs Pipeline
| 特性 | 事务 (MULTI/EXEC) | Pipeline |
|---|---|---|
| 原子性 | ✅ 保证 | ❌ 不保证 |
| 排队阶段 | 命令排队,返回 QUEUED | 命令直接发送 |
| 错误处理 | 排队错误中止整个事务 | 逐个处理 |
| 性能 | 略低(需要排队) | 更高 |
| WATCH 支持 | ✅ | ❌ |
| 适用场景 | 需要原子保证 | 批量操作 |
组合使用
Pipeline + 事务可以减少 RTT:
pipe = r.pipeline(transaction=True) # 默认就使用事务
# 这实际上会发送:MULTI, 命令1, 命令2, ..., EXEC
# 但在一个 Pipeline 批次中完成
pipe.set("key1", "value1")
pipe.set("key2", "value2")
results = pipe.execute() # 发送 EXEC
7.13 注意事项
⚠️ 事务不支持回滚 命令执行失败不会回滚已成功的命令。如果需要更强的事务保证,考虑使用 Lua 脚本。
⚠️ WATCH 的重试 在高并发场景下,WATCH 冲突可能频繁发生。建议实现指数退避重试策略。
⚠️ 事务中的命令是顺序执行的 事务中的命令按顺序执行,但不保证不被其他客户端的命令穿插(除非使用 WATCH)。
⚠️ 集群中的事务 事务中的所有 key 必须在同一个哈希槽(hash slot)中,否则事务无法在集群模式下执行。
7.14 扩展阅读
| 资源 | 说明 |
|---|---|
| Redis Transactions 文档 | 官方文档 |
| WATCH 命令 | WATCH 详细说明 |
| Redis 事务 vs Lua | 比较 |