🔄 Redis 事务

保证操作原子性的命令组合机制

事务概述

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