事务概述
Redis 事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis 事务的主要作用就是串联多个命令防止别的命令插队。虽然 Redis 事务不完全符合传统数据库的 ACID 特性,但它提供了一定程度的原子性保证。
Redis 事务执行流程
1️⃣ 开启事务
使用 MULTI 命令开始事务
→
2️⃣ 命令入队
后续命令进入事务队列
→
3️⃣ 执行事务
使用 EXEC 执行所有命令
**特点:**命令队列化、顺序执行、不可中断、部分原子性
⚛️
原子性 (Atomicity)
❌ 部分支持
单个命令原子性,但事务整体不保证
🔒
一致性 (Consistency)
✅ 支持
事务执行前后数据一致
🔐
隔离性 (Isolation)
✅ 支持
事务执行期间不被打断
💾
持久性 (Durability)
✅ 支持
依赖 Redis 持久化机制
🛠️ 事务基本命令
Redis 事务的核心命令及其使用方法:
| 命令 | 语法 | 描述 | 返回值 |
|---|---|---|---|
| MULTI | MULTI | 开启事务,后续命令进入队列 | OK |
| EXEC | EXEC | 执行事务中的所有命令 | 命令执行结果数组 |
| DISCARD | DISCARD | 取消事务,清空命令队列 | OK |
| WATCH | WATCH key [key ...] | 监视键,实现乐观锁 | OK |
| UNWATCH | UNWATCH | 取消监视所有键 | OK |
基本事务示例:
# 开启事务
MULTI
# 返回:OK
# 添加命令到事务队列
SET account:1 100
# 返回:QUEUED
SET account:2 200
# 返回:QUEUED
INCR counter
# 返回:QUEUED
GET account:1
# 返回:QUEUED
# 执行事务
EXEC
# 返回:
# 1) OK
# 2) OK
# 3) (integer) 1
# 4) "100"
# 查看结果
GET account:1
# 返回:"100"
GET account:2
# 返回:"200"
GET counter
# 返回:"1"
取消事务示例:
# 开启事务
MULTI
# 返回:OK
# 添加命令
SET key1 "value1"
# 返回:QUEUED
SET key2 "value2"
# 返回:QUEUED
# 取消事务
DISCARD
# 返回:OK
# 检查键是否存在
GET key1
# 返回:(nil) # 事务被取消,命令未执行
GET key2
# 返回:(nil)
👁️ WATCH 命令和乐观锁
WATCH 命令用于监视一个或多个键,如果在事务执行之前这些键被其他命令修改,那么事务将被打断:
🔍 监视状态
- WATCH 生效:键未被修改
- 事务正常执行:返回命令结果
- 监视自动取消:EXEC 后清除
⚠️ 监视失效
- 键被修改:其他客户端修改了监视的键
- 事务被打断:EXEC 返回 nil
- 需要重试:重新执行整个流程
🔄 重试机制
- 检测失败:判断 EXEC 返回值
- 重新监视:再次 WATCH 键
- 重新执行:重复事务逻辑
乐观锁示例 - 银行转账:
# 初始化账户余额
SET account:A 1000
SET account:B 500
# 客户端1:转账操作
# 监视两个账户
WATCH account:A account:B
# 获取当前余额
GET account:A
# 返回:"1000"
GET account:B
# 返回:"500"
# 开启事务
MULTI
# 转账 200 元从 A 到 B
DECRBY account:A 200
INCRBY account:B 200
# 执行事务
EXEC
# 返回:
# 1) (integer) 800
# 2) (integer) 700
# 验证结果
GET account:A
# 返回:"800"
GET account:B
# 返回:"700"
监视失效示例:
# 客户端1:开始转账
WATCH account:A account:B
GET account:A # 1000
GET account:B # 500
# 此时客户端2修改了账户A
# 客户端2:
SET account:A 1500
# 客户端1:继续执行事务
MULTI
DECRBY account:A 200
INCRBY account:B 200
EXEC
# 返回:(nil) # 事务被打断
# 检查账户状态
GET account:A
# 返回:"1500" # 被客户端2修改的值
GET account:B
# 返回:"500" # 未被修改
# 需要重新执行转账逻辑
完整的乐观锁重试机制:
# Python 示例代码
import redis
import time
def transfer_money(r, from_account, to_account, amount, max_retries=3):
"""
使用乐观锁实现安全的转账操作
"""
for attempt in range(max_retries):
try:
# 监视账户
r.watch(from_account, to_account)
# 获取当前余额
from_balance = int(r.get(from_account) or 0)
to_balance = int(r.get(to_account) or 0)
# 检查余额是否足够
if from_balance < amount:
r.unwatch()
return False, "余额不足"
# 开启事务
pipe = r.pipeline()
pipe.multi()
# 执行转账
pipe.decrby(from_account, amount)
pipe.incrby(to_account, amount)
# 执行事务
result = pipe.execute()
if result is not None:
return True, f"转账成功,尝试次数:{attempt + 1}"
else:
print(f"第 {attempt + 1} 次尝试失败,重试中...")
time.sleep(0.01) # 短暂等待
except redis.WatchError:
print(f"第 {attempt + 1} 次尝试失败,键被修改")
time.sleep(0.01)
return False, "转账失败,超过最大重试次数"
# 使用示例
r = redis.Redis()
success, message = transfer_money(r, "account:A", "account:B", 200)
print(message)
⚠️ 事务错误处理
Redis 事务中可能出现两种类型的错误:
🚫 编译时错误
**发生时机:**命令入队时
**错误类型:**语法错误、命令不存在
**处理方式:**整个事务被取消
# 示例
MULTI
SET key1 value1
SETX key2 value2 # 错误命令
# 返回:(error) ERR unknown command 'SETX'
EXEC
# 返回:(error) EXECABORT Transaction discarded
💥 运行时错误
**发生时机:**EXEC 执行时
**错误类型:**类型错误、操作错误
**处理方式:**错误命令失败,其他命令继续
# 示例
SET mykey "hello"
MULTI
INCR mykey # 对字符串执行数值操作
SET key2 value2 # 正常命令
EXEC
# 返回:
# 1) (error) ERR value is not an integer
# 2) OK
🚨 重要注意事项:无回滚机制:Redis 不支持事务回滚部分失败:运行时错误不会影响其他命令错误检查:需要检查每个命令的执行结果数据一致性:需要应用层保证逻辑一致性
🎯 应用场景
Redis 事务在实际项目中的典型应用场景:
💰 金融转账
# 银行转账操作
WATCH account:from account:to
# 检查余额
GET account:from
GET account:to
# 执行转账
MULTI
DECRBY account:from 1000
INCRBY account:to 1000
SADD transaction:log "transfer:1000:from:to"
EXEC
# 记录转账日志
LPUSH transfer:history "$(date): 1000 from -> to"
**优势:**保证转账原子性,防止数据不一致
🛒 库存扣减
# 商品库存扣减
WATCH product:123:stock
# 检查库存
GET product:123:stock
# 扣减库存
MULTI
DECR product:123:stock
SADD order:items "product:123"
INCR product:123:sold
EXEC
# 检查扣减结果
GET product:123:stock
**优势:**防止超卖,保证库存准确性
📊 计数器更新
# 网站访问统计
MULTI
INCR site:visits:total
INCR site:visits:today
INCR page:home:views
SADD visitors:today "user:123"
ZINCRBY popular:pages 1 "home"
EXEC
# 用户行为统计
MULTI
INCR user:123:actions
LPUSH user:123:history "login:$(date)"
SET user:123:last_seen "$(date)"
EXEC
**优势:**批量更新统计数据,保证一致性
🎮 游戏状态更新
# 玩家升级操作
WATCH player:123:level player:123:exp
# 获取当前状态
GET player:123:level
GET player:123:exp
# 升级操作
MULTI
INCR player:123:level
SET player:123:exp 0
INCR player:123:skill_points
SADD achievements:123 "level_up"
EXEC
# 排行榜更新
ZADD leaderboard player:123:level player:123
**优势:**保证游戏状态一致性,防止作弊
🔐 分布式锁
# 获取分布式锁
SET lock:resource:123 "client_id" EX 30 NX
# 使用事务释放锁
WATCH lock:resource:123
GET lock:resource:123
# 检查锁的所有者
if lock_owner == client_id:
MULTI
DEL lock:resource:123
EXEC
else:
UNWATCH
**优势:**安全释放锁,防止误删其他客户端的锁
📝 批量数据操作
# 批量用户数据更新
MULTI
HMSET user:123 name "张三" age 25 city "北京"
SADD city:beijing:users "user:123"
ZADD users:by_age 25 "user:123"
INCR stats:total_users
EXEC
# 批量删除过期数据
MULTI
DEL cache:expired:key1
DEL cache:expired:key2
DEL cache:expired:key3
DECRBY cache:count 3
EXEC
**优势:**提高批量操作效率,减少网络往返
事务性能和最佳实践
| 方面 | 优点 | 缺点 | 最佳实践 |
|---|---|---|---|
| 原子性 | 命令队列化执行 | 无真正的回滚机制 | 应用层检查结果 |
| 性能 | 减少网络往返 | 阻塞其他操作 | 控制事务大小 |
| 并发 | 乐观锁机制 | 高并发时重试频繁 | 合理设计重试策略 |
| 错误处理 | 语法错误自动取消 | 运行时错误不回滚 | 预先验证数据 |
✅ 最佳实践建议:
- 事务大小:控制事务中命令数量,避免长时间阻塞
- 错误检查:检查 EXEC 返回值,处理执行失败情况
- 重试机制:实现合理的重试策略,避免无限重试
- 监视键选择:只监视必要的键,减少冲突概率
- 数据验证:在事务前验证数据类型和范围
- 超时处理:设置合理的超时时间,避免死锁
- 日志记录:记录事务执行情况,便于问题排查
💡 使用建议:适用场景:需要批量操作、数据一致性要求不是特别严格不适用场景:需要严格 ACID 特性的金融级应用替代方案:Lua 脚本可以提供更好的原子性保证监控指标:监控事务成功率、重试次数、执行时间