找回密码
 立即注册
首页 业界区 安全 Redis事务详解

Redis事务详解

汪之亦 2025-10-1 11:54:30
1、概念

Redis 事务的本质是将多个命令打包,然后按顺序、一次性、隔离地执行。它通过 MULTI, EXEC, DISCARD, WATCH 四个核心命令实现。

  • MULTI:开启一个事务,之后的命令都会放入队列,而不是立即执行。
  • EXEC:执行事务队列中的所有命令。
  • DISCARD:取消事务,清空队列。
  • WATCH:在 MULTI 之前,监视一个或多个 key。如果在 EXEC 执行前,这些被监视的 key 被其他客户端修改,则整个事务会失败(EXEC 返回 nil)。这是实现乐观锁(Optimistic Locking) 的基础。
与传统事务的差异

  • 无回滚:Redis 设计哲学是简单和高效,失败的命令通常是编程错误(命令用错、数据类型错误),应该在开发阶段被发现,而不是在生产环境通过回滚来挽救。回滚会增加复杂性和性能开销。
  • 隔离性:事务中的所有命令都会按顺序串行执行,并且在 EXEC 执行前,事务中的命令不会被其他客户端的命令插队。这保证了事务执行过程中不会受到其他客户端的干扰。
  • 持久化无关:事务的 “执行成功” 仅表示命令入队并执行,不保证已持久化到磁盘(需配合 AOF 配置 appendfsync always 提升可靠性,但会牺牲性能)。
  • 不保证原子性(Atomicity):这是最大的误区!Redis 事务保证的是隔离性(Isolation) 和顺序执行,但不保证原子性。这意味着如果事务中某个命令执行失败(例如对字符串执行了 HINCRBY),其他命令依然会继续执行,且不会回滚(Rollback)
2、最佳实践详解与示例

2.1 一个失败的事务会发生什么?
  1. 127.0.0.1:6379> SET key1 "hello"
  2. OK
  3. 127.0.0.1:6379> SET key2 100
  4. OK
  5. 127.0.0.1:6379> MULTI
  6. OK
  7. 127.0.0.1:6379> INCR key1      # 错误:对字符串 'hello' 执行 INCR,这会失败!
  8. QUEUED
  9. 127.0.0.1:6379> INCR key2      # 正确:对数字 100 执行 INCR
  10. QUEUED
  11. 127.0.0.1:6379> EXEC
  12. 1) (error) ERR value is not an integer or out of range # 第一条命令报错
  13. 2) (integer) 101                                      # 第二条命令成功执行!
  14. 127.0.0.1:6379> GET key2
  15. "101"
复制代码
2.2 使用WATCH实现乐观锁

工作流程:

  • WATCH 需要监控的 key。
  • 读取 key 的值并进行业务逻辑计算。
  • 开启 MULTI。
  • 将写命令(如 SET, INCRBY)放入队列。
  • 执行 EXEC。
  • 如果成功:说明在 WATCH 到 EXEC 期间,被监控的 key 没有被其他客户端改动,事务执行成功,WATCH 可以监控多个 key,只要其中任何一个被修改,事务都会失败。
  • 如果返回 nil:说明 key 已被修改,事务执行失败。通常的处理方式是重试整个流程。
  1. import redis
  2. import time
  3. r = redis.Redis(host='localhost', port=6379, db=0)
  4. def deduct_balance(user_id, amount):
  5.     """ 扣减用户余额,使用乐观锁保证并发安全 """
  6.     balance_key = f"user:{user_id}:balance"
  7.     # 重试次数,避免死循环
  8.     max_retries = 5
  9.     retry_count = 0
  10.     while retry_count < max_retries:
  11.         retry_count += 1
  12.         try:
  13.             # 1. 开启 Pipeline(包含 WATCH 命令,Python redis库中,pipeline 默认隐含 MULTI/EXEC)
  14.             # `transaction=True` 表示我们要使用事务,它内部会处理 WATCH/MULTI/EXEC
  15.             pipe = r.pipeline(transaction=True)
  16.             
  17.             # 2. WATCH 要监控的 key
  18.             pipe.watch(balance_key)
  19.             
  20.             # 3. 读取并检查余额
  21.             current_balance = int(pipe.get(balance_key) or 0)
  22.             if current_balance < amount:
  23.                 pipe.unwatch() # 解除监控,可选,EXEC/DISCARD后也会自动解除
  24.                 print("Insufficient balance!")
  25.                 return False
  26.             # 4. 开启事务,组装命令
  27.             pipe.multi()
  28.             pipe.decrby(balance_key, amount)
  29.             
  30.             # 5. 执行事务
  31.             # 如果 balance_key 在 WATCH 后被执行了,这里会抛出 WatchError 异常
  32.             result = pipe.execute()
  33.             # execute() 成功返回命令执行结果的列表,例如 [95]
  34.             print(f"Deduction successful! New balance: {result[0]}")
  35.             return True
  36.             
  37.         except redis.WatchError:
  38.             # 6. 捕获 WatchError,表示事务失败,进行重试
  39.             print(f"Transaction conflicted. Retrying... ({retry_count}/{max_retries})")
  40.             time.sleep(0.1) # 稍等片刻再重试,避免激烈竞争
  41.             continue
  42.             
  43.         except Exception as e:
  44.             # 处理其他异常
  45.             print(f"Unexpected error: {e}")
  46.             break
  47.     print("Failed after max retries.")
  48.     return False
  49. # 初始化用户余额
  50. r.set("user:100:balance", 100)
  51. # 模拟并发扣减
  52. deduct_balance(100, 50)
复制代码
3、使用 Lua 脚本替代复杂事务

对于非常复杂的逻辑,使用 WATCH 和 MULTI 会使得重试循环非常笨重。此时,Lua 脚本是更好的选择。
Lua 脚本的优势:

  • 真正的原子性:整个脚本在执行时是以单线程、原子性的方式运行的,中间不会被执行任何其他命令,无需使用 WATCH。
  • 减少网络开销:逻辑在服务器端执行,避免了多次网络往返。
  • 复杂性封装:将复杂逻辑封装在服务器端的一个脚本中。
  1. import redis
  2. r = redis.Redis(host='localhost', port=6379, db=0)
  3. # 定义Lua脚本代码
  4. LUA_DEDUCT_SCRIPT = """
  5. local balance_key = KEYS[1]
  6. local amount = tonumber(ARGV[1])
  7. local current_balance = tonumber(redis.call('get', balance_key) or 0)
  8. if current_balance < amount then
  9.     return false -- 余额不足,返回false
  10. end
  11. redis.call('decrby', balance_key, amount)
  12. return true -- 扣减成功,返回true
  13. """
  14. # 预先加载脚本,获取一个sha1哈希摘要,后续可以用这个摘要来执行,效率更高。
  15. script_sha = r.script_load(LUA_DEDUCT_SCRIPT)
  16. def deduct_balance_lua(user_id, amount):
  17.     balance_key = f"user:{user_id}:balance"
  18.     # 使用 evalsha 执行脚本
  19.     # 1 表示后面有1个KEY, amount 是ARGV参数
  20.     success = r.evalsha(script_sha, 1, balance_key, amount)
  21.     return bool(success)
  22. # 初始化
  23. r.set("user:100:balance", 100)
  24. # 执行扣减
  25. result = deduct_balance_lua(100, 50)
  26. print(f"Deduction result: {result}")
  27. print(f"Remaining balance: {r.get('user:100:balance')}")
复制代码
最佳实践:

  • 对于复杂的、需要原子性执行的逻辑,优先使用 Lua 脚本。
  • 使用 SCRIPT LOAD 预加载脚本,然后用 EVALSHA 通过脚本的 SHA1 摘要来执行,可以节省每次传输完整脚本的网络带宽。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册