Redis 事务(Transaction)完全攻略
“半事务”机制:原子执行 + 隔离性,但 无回滚
适合 批量操作、乐观锁、状态机切换,不适合复杂业务逻辑
一、Redis 事务核心特点
| 特性 | 说明 |
|---|---|
| 命令 | MULTI → EXEC → DISCARD |
| 原子性 | 所有命令一起执行或都不执行 |
| 隔离性 | 命令入队时不执行,EXEC 时一次性执行 |
| 无回滚 | 出错不回滚,前面命令已生效 |
| 无 ACID 完整性 | 仅 AI,无 C(一致性)、D(持久性)保证 |
| 乐观锁 | 配合 WATCH 实现 |
Redis 事务 ≠ 数据库事务,更像 批量原子操作
二、事务命令全表
| 命令 | 说明 | 示例 |
|---|---|---|
MULTI | 开启事务 | MULTI |
EXEC | 执行事务 | EXEC |
DISCARD | 取消事务 | DISCARD |
WATCH key [key ...] | 监控 key(乐观锁) | WATCH balance:1001 |
UNWATCH | 取消监控 | UNWATCH |
三、事务执行流程
MULTI
INCR counter
HSET user:1 score 100
EXEC
# 返回: 1) 2 2) OK
graph TD
A[客户端] -->|MULTI| B[Redis 事务队列]
B -->|命令入队| C[INCR, HSET, ...]
C -->|EXEC| D[一次性原子执行]
D -->|成功| E[返回结果数组]
D -->|失败| F[部分成功,前面命令已生效]
四、核心实战场景
1. 批量操作(提升性能)
MULTI
SET key1 "v1"
SET key2 "v2"
INCR counter
EXEC
比 Pipeline 多了 原子性,但性能略低
2. 转账(乐观锁 + WATCH)
# 步骤1:监控账户余额
WATCH balance:1001 balance:1002
# 步骤2:读取余额
GET balance:1001 → 1000
GET balance:1002 → 500
# 步骤3:开启事务
MULTI
DECRBY balance:1001 100
INCRBY balance:1002 100
EXEC
# 如果中途被修改 → EXEC 返回 nil
失败重试机制(客户端实现)
def transfer():
while True:
r.watch('balance:1001', 'balance:1002')
a = int(r.get('balance:1001') or 0)
b = int(r.get('balance:1002') or 0)
if a < 100:
r.unwatch()
return False
pipe = r.multi()
pipe.decrby('balance:1001', 100)
pipe.incrby('balance:1002', 100)
result = pipe.execute()
if result: # 成功
return True
# 失败 → 重试
3. 状态机切换
MULTI
SET order:1001:status "paid"
DEL order:1001:cart
EXEC
4. 库存扣减(结合 Lua 更佳)
MULTI
DECRBY stock:1001 1
ZADD recent:sales:1001 {timestamp} "user:2001"
EXEC
推荐用 Lua 脚本替代复杂事务
五、事务 vs Pipeline vs Lua 脚本 对比
| 对比项 | 事务 (MULTI) | Pipeline | Lua 脚本 |
|---|---|---|---|
| 原子性 | 支持 | 不支持 | 支持 |
| 回滚 | 不支持 | 不支持 | 不支持 |
| 性能 | 中等 | 最快 | 快 |
| 逻辑复杂 | 不支持 | 不支持 | 支持 |
| 错误处理 | 部分执行 | 全部执行 | 全部失败 |
| 推荐场景 | 简单原子批量 | 高性能批量 | 复杂业务逻辑 |
结论:
- 简单原子操作 → 事务
- 高性能批量 → Pipeline
- 复杂逻辑 → Lua 脚本
六、事务失败场景解析
| 场景 | 结果 |
|---|---|
| 语法错误 | 整个事务失败(EXEC 前发现) |
运行时错误(如 INCR 非数字) | 前面命令已执行,错误命令返回错误 |
| WATCH 冲突 | EXEC 返回 nil |
| 客户端断开 | 事务被丢弃 |
MULTI
SET a 1
INCR a # 运行时错误
SET b 2
EXEC
# 返回: 1) OK 2) ERR value is not integer 3) OK
七、WATCH 乐观锁详解
graph TD
A[客户端1] -->|WATCH k| R[Redis]
B[客户端2] -->|SET k v2| R
A -->|MULTI| R
A -->|SET k v3| R
A -->|EXEC| R -->|返回 nil| A
最佳实践:
WATCH+MULTI+ 重试- 监控 最小必要 key
- 避免长时间
WATCH
八、一键速查表
# 基本事务
MULTI
SET a 1
INCR counter
EXEC
# 乐观锁转账
WATCH balance:1
GET balance:1
MULTI
DECRBY balance:1 100
INCRBY balance:2 100
EXEC # 若返回 nil → 重试
# 取消
DISCARD
UNWATCH
九、客户端代码示例
Python (redis-py)
import redis
r = redis.Redis()
# 简单事务
pipe = r.multi()
pipe.set('k1', 'v1')
pipe.incr('counter')
result = pipe.execute()
print(result) # [True, 2]
# 乐观锁转账
def transfer(r, from_key, to_key, amount):
while True:
with r.pipeline() as pipe:
try:
pipe.watch(from_key, to_key)
balance = int(pipe.get(from_key) or 0)
if balance < amount:
pipe.unwatch()
return False
pipe.multi()
pipe.decrby(from_key, amount)
pipe.incrby(to_key, amount)
pipe.execute()
return True
except redis.WatchError:
continue # 重试
十、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 部分命令失败 | 运行时错误 | 用 Lua 脚本 |
| 死锁 | WATCH 太多 | 减少监控 key |
| 性能低 | 事务太大 | 拆分 + Pipeline |
| 事务返回 nil | WATCH 冲突 | 客户端重试 |
十一、高并发事务优化
1. 短事务原则
# 错误:1000 个命令
MULTI
HSET ... x1000
EXEC
# 正确:分批
for i in range(0, 1000, 100):
pipe = r.multi()
for j in range(100):
pipe.hset(...)
pipe.execute()
2. Lua 替代复杂事务
EVAL "
if redis.call('exists', KEYS[1]) == 1 then
local stock = tonumber(redis.call('get', KEYS[1]))
if stock >= 1 then
redis.call('decr', KEYS[1])
return 1
end
end
return 0
" 1 stock:1001
完成!你已精通 Redis 事务!
# 一行命令体验事务全流程
redis-cli <<EOF
WATCH counter
GET counter
MULTI
INCR counter
EXEC
UNWATCH
EOF
下一步推荐:
需要我送你:
- “高并发转账系统(事务 + 重试)”?
- “库存扣减原子性方案(事务 vs Lua)”?
- “状态机事务模板”?
回复:转账 | 扣库存 | 状态机 即可!