事务概述
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 脚本可以提供更好的原子性保证
- 监控指标:监控事务成功率、重试次数、执行时间