news 2026/6/4 9:11:31

【Redis篇】Redis 事务:原子性与脚本执行机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Redis篇】Redis 事务:原子性与脚本执行机制

文章目录

    • Redis 事务:原子性与脚本执行机制
    • 一、前言
    • 二、Redis 事务的基本概念
      • 2.1 为什么需要 Redis 事务
      • 2.2 Redis 事务的三个命令
    • 三、事务的完整流程
      • 3.1 基本使用示例
      • 3.2 放弃事务
    • 四、事务中的错误处理
      • 4.1 第一类错误:命令语法错误(编译期错误)
      • 4.2 第二类错误:命令运行时错误(执行期错误)
      • 4.3 Redis 事务的"弱原子性"
    • 五、WATCH 命令与乐观锁
      • 5.1 事务存在的竞态问题
      • 5.2 WATCH 命令
      • 5.3 基于 WATCH 实现乐观锁的完整示例
      • 5.4 UNWATCH 命令
    • 六、Lua 脚本
      • 6.1 Redis 事务的局限性
      • 6.2 Lua 脚本的核心优势
      • 6.3 EVAL 命令语法
      • 6.4 使用示例
      • 6.5 SCRIPT LOAD 与 EVALSHA
      • 6.6 Lua 脚本的注意事项
    • 七、事务 vs Lua 脚本
    • 八、总结

Redis 事务:原子性与脚本执行机制

一、前言

💬这一篇讲什么:Redis 的事务机制与 Lua 脚本

🚀核心内容

  • Redis 事务的基本流程:MULTI / EXEC / DISCARD
  • 事务中出现错误怎么处理?Redis 事务的"弱原子性"是什么意思?
  • WATCH 命令如何实现乐观锁?
  • Redis 事务的局限性是什么?
  • 为什么 Lua 脚本能弥补事务的不足?Lua 脚本的使用方式

上一篇讲完了 Redis 持久化,这一篇来看 Redis 的事务机制。很多同学从 MySQL 事务的认知出发去理解 Redis 事务,会踩不少坑——Redis 事务和 MySQL 事务差别很大,它并不支持回滚。搞清楚这些差异,才能正确地在业务中使用 Redis 事务。


二、Redis 事务的基本概念

2.1 为什么需要 Redis 事务

Redis 是单线程处理命令的,单条命令的执行是原子的。但很多业务场景需要把多条命令组合在一起原子地执行,中间不能被其他客户端的命令打断,这就是 Redis 事务要解决的问题。

经典例子:转账操作需要同时减少 A 的余额、增加 B 的余额,这两步要么都成功,要么都不执行。

2.2 Redis 事务的三个命令

Redis 事务用三个命令控制:

命令作用
MULTI开启事务,进入事务模式
EXEC提交事务,顺序执行事务中排队的所有命令
DISCARD丢弃事务,清空命令队列,退出事务模式(相当于"回滚")

三、事务的完整流程

3.1 基本使用示例

127.0.0.1:6379>MULTI# 开启事务OK127.0.0.1:6379>SET k1 v1# 命令进入队列(不立即执行)QUEUED127.0.0.1:6379>SET k2 v2 QUEUED127.0.0.1:6379>INCR counter QUEUED127.0.0.1:6379>EXEC# 提交事务,顺序执行所有命令1)OK2)OK3)(integer)1

执行MULTI后,Redis 会返回QUEUED而不是立即执行命令,所有命令都放入一个命令队列中。调用EXEC后,Redis 顺序执行队列中的所有命令并返回结果数组。

3.2 放弃事务

执行DISCARD可以放弃当前事务,清空命令队列:

127.0.0.1:6379>MULTI OK127.0.0.1:6379>SET k1 v1 QUEUED127.0.0.1:6379>SET k2 v2 QUEUED127.0.0.1:6379>DISCARD# 放弃事务OK# 事务中的命令全部取消,k1 和 k2 均未被修改

四、事务中的错误处理

这是理解 Redis 事务最重要的部分。Redis 事务对错误的处理方式与 MySQL 完全不同,且两类错误的处理方式也不一样

4.1 第一类错误:命令语法错误(编译期错误)

如果在 MULTI 和 EXEC 之间输入了一条语法错误的命令(比如命令名写错了),Redis 会立即返回错误,并在执行 EXEC 时放弃整个事务

127.0.0.1:6379>MULTI OK127.0.0.1:6379>SET k1 v1 QUEUED127.0.0.1:6379>SETXXX k2 v2# 不存在的命令,语法错误(error)ERR unknowncommand'SETXXX'127.0.0.1:6379>SET k3 v3 QUEUED127.0.0.1:6379>EXEC(error)EXECABORT Transaction discarded because of previous errors.# 整个事务被放弃,k1 和 k3 均未被修改

这类错误,整个事务不执行。表现和我们预期的"事务回滚"接近。

4.2 第二类错误:命令运行时错误(执行期错误)

如果命令语法正确,但执行时因为数据类型不匹配等原因失败,Redis 会继续执行其他命令,不会回滚

127.0.0.1:6379>SET mystr"hello"# mystr 是字符串类型OK127.0.0.1:6379>MULTI OK127.0.0.1:6379>SET k1 v1 QUEUED127.0.0.1:6379>INCR mystr# 对字符串执行自增,运行时才会报错QUEUED127.0.0.1:6379>SET k2 v2 QUEUED127.0.0.1:6379>EXEC1)OK# SET k1 v1 执行成功2)(error)ERR value...# INCR mystr 执行失败,但不影响其他命令3)OK# SET k2 v2 执行成功

这类错误,只有出错的那条命令失败,其他命令继续正常执行。

4.3 Redis 事务的"弱原子性"

从上面两种错误处理可以看出:Redis 事务不支持回滚(Rollback)

在 MySQL 中,只要事务中任意一步出错,整个事务都会回滚,保证"要么全成功,要么全失败"。Redis 的事务做不到这一点:一旦 EXEC 开始执行,即使中途某条命令失败,其他命令也会继续执行,已经成功的命令不会被撤销

因此,Redis 事务被称为**“弱原子性”**:

  • ✅ 保证了事务中的命令不会被其他客户端的命令穿插打断(隔离性)
  • ✅ 保证了命令一定会被顺序执行(有序性)
  • ❌ 不保证全部成功或全部失败(不支持回滚)

为什么 Redis 不支持回滚?Redis 官方给出的理由是:运行时错误通常是编程错误(比如把字符串当数字用),正确编写的程序不应该出现这类错误。不支持回滚使得 Redis 的实现更简单,性能更好。


五、WATCH 命令与乐观锁

5.1 事务存在的竞态问题

假设要实现一个功能:只有当账户余额balance >= 100时,才执行扣款操作(减去 100)。用事务写出来可能是:

GET balance# 读取余额,假设返回 200MULTI DECR balance100EXEC

问题在于:GET balanceMULTI...EXEC之间存在时间窗口,另一个客户端可能在这个窗口内也读到了 balance=200,也发起了扣款,两个客户端都成功执行,最终 balance 变成了 0,而不是预期的 100。

这就是**Check-Then-Act(先检查后操作)**的竞态条件,普通事务无法解决。

5.2 WATCH 命令

WATCH命令为 Redis 事务提供了乐观锁机制:

WATCH key[key...]

WATCH监视一个或多个 key。在 MULTI 之前执行 WATCH,如果在 MULTI 到 EXEC 期间,被监视的 key 被任何其他客户端修改,执行 EXEC 时整个事务会自动失败,返回 nil(而不是命令结果数组)。

# 客户端 A127.0.0.1:6379>WATCH balance# 监视 balanceOK127.0.0.1:6379>GET balance# 读取余额 = 200"200"127.0.0.1:6379>MULTI OK127.0.0.1:6379>DECRBY balance100QUEUED# === 此时客户端 B 修改了 balance ===# 客户端 B: SET balance 50127.0.0.1:6379>EXEC# 回到客户端 A 提交事务(nil)# 返回 nil,事务失败,balance 未被修改

5.3 基于 WATCH 实现乐观锁的完整示例

实际业务中,配合重试逻辑使用 WATCH:

importredis r=redis.Redis()whileTrue:withr.pipeline()aspipe:try:pipe.watch('balance')# 监视 balancebalance=int(pipe.get('balance'))ifbalance<100:print("余额不足,取消操作")pipe.unwatch()# 取消监视breakpipe.multi()# 开启事务pipe.decrby('balance',100)pipe.execute()# 提交事务print("扣款成功")breakexceptredis.WatchError:# 监视的 key 被其他客户端修改,重试print("数据被修改,重试中...")continue

WATCH 的工作原理:乐观锁,假设并发冲突不常见。不上锁,只在提交时检查是否有冲突;有冲突就放弃并重试,没冲突就直接提交。性能比悲观锁(每次操作都加锁)好得多,适合冲突概率低的场景。

5.4 UNWATCH 命令

UNWATCH

取消当前客户端对所有 key 的监视。执行 EXEC 或 DISCARD 后,WATCH 监视会自动取消,无需手动 UNWATCH。


六、Lua 脚本

6.1 Redis 事务的局限性

虽然 WATCH + 事务可以实现乐观锁,但 Redis 原生事务有一个根本的局限性:无法在事务执行期间根据命令结果来决定下一步操作。

比如:如果 key 不存在就 SET,如果已存在就 INCR。用事务写不了,因为 MULTI 进入队列阶段无法读取命令结果,无法做条件判断。

Lua 脚本完美解决了这个问题。

6.2 Lua 脚本的核心优势

Redis 内置了 Lua 解释器,通过EVAL命令可以执行 Lua 脚本。Lua 脚本在 Redis 中具有以下关键特性:

原子性:Lua 脚本在 Redis 中是原子执行的。执行期间,其他客户端的命令不会被执行,不存在并发问题。这是比原生事务更强的保证。

支持条件判断:Lua 脚本是完整的编程语言,可以在脚本中读取 Redis 数据,然后根据数据决定执行什么操作,原生事务做不到这一点。

减少网络往返:多条命令打包在一个 Lua 脚本中,只需要一次网络请求,比多次 COMMAND 调用效率更高。

6.3 EVAL 命令语法

EVAL script numkeys key[key...]arg[arg...]

参数说明:

  • script:Lua 脚本内容
  • numkeys:传入的 key 数量
  • key [key ...]:传给脚本的 key 名称,在脚本中通过KEYS[1]KEYS[2]访问(下标从 1 开始)
  • arg [arg ...]:传给脚本的额外参数,在脚本中通过ARGV[1]ARGV[2]访问

在 Lua 脚本中调用 Redis 命令的方式:

redis.call('命令名',参数1,参数2,...)redis.pcall('命令名',参数1,参数2,...)-- 出错时不抛异常,返回错误信息

6.4 使用示例

示例一:基本使用

# 设置一个 key,值为传入的参数127.0.0.1:6379>EVAL"return redis.call('SET', KEYS[1], ARGV[1])"1mykey hello OK127.0.0.1:6379>GET mykey"hello"

示例二:条件判断(事务做不到的事)

# 如果 key 不存在则设置值,如果已存在则自增# 这在原生事务中无法实现,但 Lua 脚本可以轻松做到127.0.0.1:6379>EVAL" if redis.call('EXISTS', KEYS[1]) == 0 then return redis.call('SET', KEYS[1], ARGV[1]) else return redis.call('INCR', KEYS[1]) end "1counter100OK127.0.0.1:6379>EVAL" if redis.call('EXISTS', KEYS[1]) == 0 then return redis.call('SET', KEYS[1], ARGV[1]) else return redis.call('INCR', KEYS[1]) end "1counter100(integer)101

示例三:用 Lua 实现原子性的库存扣减

127.0.0.1:6379>EVAL" local stock = tonumber(redis.call('GET', KEYS[1])) if stock == nil or stock <= 0 then return 0 end redis.call('DECRBY', KEYS[1], ARGV[1]) return 1 "1product:1001:stock1(integer)1-- 返回1表示扣减成功,返回0表示库存不足

6.5 SCRIPT LOAD 与 EVALSHA

如果一个 Lua 脚本需要频繁执行,每次都传输完整的脚本内容会浪费带宽。可以先加载脚本,得到一个 SHA1 摘要,后续用摘要来执行:

# 加载脚本到 Redis,返回 SHA1 摘要127.0.0.1:6379>SCRIPT LOAD"return redis.call('GET', KEYS[1])""e0e1f9fabfa9d353e4f3df77bf9d21d32dbb72b5"# 用 SHA1 执行脚本(传输数据量更小)127.0.0.1:6379>EVALSHA e0e1f9fabfa9d353e4f3df77bf9d21d32dbb72b51mykey"hello"# 检查某个脚本是否已加载127.0.0.1:6379>SCRIPT EXISTS e0e1f9fabfa9d353e4f3df77bf9d21d32dbb72b51)(integer)1# 清空所有已缓存的脚本127.0.0.1:6379>SCRIPT FLUSH OK

6.6 Lua 脚本的注意事项

不要在 Lua 脚本中执行耗时操作。Lua 脚本执行期间,Redis 主线程被阻塞,其他所有客户端命令都要等待。如果脚本执行了耗时的循环计算,会严重影响 Redis 的响应时间。

Redis 会超时强制终止 Lua 脚本。通过lua-time-limit配置(默认 5000ms),超过此时间还未完成的脚本会被强制停止。

Lua 脚本在 Redis 集群中有限制。脚本中涉及的所有 key 必须在同一个 slot(哈希槽)中,否则会执行失败。在集群模式下,建议使用 Hash Tags 把相关 key 固定到同一个 slot。


七、事务 vs Lua 脚本

对比维度Redis 事务Lua 脚本
原子性弱(不支持回滚)强(执行期间完全阻塞其他命令)
支持条件判断❌ 不支持✅ 支持完整的编程逻辑
错误处理运行时错误不回滚可用 pcall 捕获错误
并发安全需要配合 WATCH 实现乐观锁天然原子,无需额外处理
网络开销多次命令 + MULTI/EXEC一次请求
适用场景简单的多命令原子批量执行复杂的需要条件判断的原子操作

结论

  • 简单的多命令批量执行,且不需要条件判断 →事务(MULTI/EXEC)
  • 需要在 Redis 端做条件判断、逻辑运算 →Lua 脚本
  • 需要乐观锁保护共享数据 →WATCH + 事务,或直接用Lua 脚本

八、总结

事务三件套MULTI开启 → 命令入队列 →EXEC提交 /DISCARD放弃

两类错误不同处理:语法错误 → 整个事务放弃;运行时错误 → 仅该命令失败,其他命令继续执行

弱原子性:Redis 事务不支持回滚,这是与 MySQL 事务最大的区别

WATCH 乐观锁:监视 key,如果在 MULTI~EXEC 期间被其他客户端修改,EXEC 自动失败

Lua 脚本:原子执行,支持条件判断,弥补了原生事务的最大局限

EVALSHA:预加载脚本 + SHA1 摘要执行,减少重复传输开销

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/4 9:04:41

2026企业协作网盘选型指南:坚果云等5大主流文档协作平台深度横评

一、先说结论&#xff1a;企业文档协作平台怎么选&#xff1f; 企业协作网盘是面向组织文件存储、共享、同步、权限管理和多人协作的企业级文件管理平台。相比个人网盘&#xff0c;企业网盘更强调组织级权限、版本管理、多端同步和核心资产的保护。为了让大家直观了解各平台的…

作者头像 李华
网站建设 2026/6/4 9:04:02

3步快速搭建Suno音乐生成API:从零到部署完整指南

3步快速搭建Suno音乐生成API&#xff1a;从零到部署完整指南 【免费下载链接】Suno-API Create Music in Seconds with SunoAPI. 项目地址: https://gitcode.com/GitHub_Trending/su/Suno-API 在AI音乐创作领域&#xff0c;Suno-API是一个基于Python和FastAPI的非官方音…

作者头像 李华
网站建设 2026/6/4 9:00:52

2分钟搞定Windows苹果驱动:让iPhone USB网络共享一键畅通

2分钟搞定Windows苹果驱动&#xff1a;让iPhone USB网络共享一键畅通 【免费下载链接】Apple-Mobile-Drivers-Installer Powershell script to easily install Apple USB and Mobile Device Ethernet (USB Tethering) drivers on Windows! 项目地址: https://gitcode.com/gh_…

作者头像 李华