【Redis】事务与Lua脚本Day7(2026年)

写在前面

Redis的事务机制与关系型数据库有很大不同,它没有完整的ACID特性,这让很多开发者感到困惑。同时,Lua脚本作为Redis的原子执行能力,在复杂业务场景中发挥着重要作用。今天我们深入探讨Redis事务和Lua脚本的原理与应用。

文章目录


一、Redis事务基础

1.1 什么是Redis事务?

实际场景:电商秒杀场景中,需要先检查库存、再扣减库存、最后记录订单,这三个操作必须作为一个整体执行,否则可能出现超卖问题。

Redis事务是一组命令的集合,这些命令会被顺序执行,执行过程中不会被其他客户端的命令打断。但需要注意的是,Redis事务不支持回滚,这与MySQL等关系型数据库有本质区别。

1.2 事务相关命令

命令 说明
MULTI 开启事务
EXEC 执行事务中的所有命令
DISCARD 取消事务
WATCH 监视一个或多个key

1.3 事务基本使用

正常执行流程:

redis 复制代码
# 开启事务
MULTI
# 返回:QUEUED

# 命令入队
SET user:1:name "zhangsan"
QUEUED
SET user:1:age 25
QUEUED
INCR counter
QUEUED

# 执行事务
EXEC
# 返回:1) OK  2) OK  3) 1

取消事务:

redis 复制代码
MULTI
SET key1 "value1"
DISCARD
# 事务取消,命令不会执行

二、WATCH命令详解

2.1 WATCH的作用

经验之谈:WATCH是实现乐观锁的关键,它可以让事务在执行前检查key是否被修改。

WATCH命令用于监视一个或多个key,如果在事务执行前这些key被其他客户端修改,则事务会被拒绝执行。

2.2 WATCH使用示例

场景:转账操作

redis 复制代码
# 客户端A:监视账户余额
WATCH account:balance
GET account:balance
# 返回:100

MULTI
DECRBY account:balance 50
EXEC
# 如果期间没有其他客户端修改balance,事务成功执行

如果期间被其他客户端修改:

redis 复制代码
# 客户端B在客户端A执行EXEC之前
SET account:balance 200

# 客户端A执行EXEC
EXEC
# 返回:(nil) 事务执行失败

2.3 WATCH实现乐观锁

复制代码
┌─────────────────────────────────────────────────┐
│                   乐观锁流程                      │
├─────────────────────────────────────────────────┤
│  1. WATCH key                                    │
│  2. GET key 获取当前值                            │
│  3. 计算新值                                      │
│  4. MULTI                                        │
│  5. SET key 新值                                 │
│  6. EXEC                                         │
│     - 成功:事务执行                              │
│     - 失败:key被修改,重试整个流程               │
└─────────────────────────────────────────────────┘

三、Redis事务的ACID分析

踩坑提醒:Redis事务不支持回滚!如果事务中某条命令执行失败,其他命令仍然会继续执行。

3.1 原子性(Atomicity)

Redis事务的原子性分析:

情况 原子性 说明
命令入队失败 满足 整个事务被拒绝执行
命令执行失败 不满足 错误命令跳过,其他继续执行
服务器宕机 不满足 已执行的命令无法回滚

示例:命令执行失败不影响其他命令

redis 复制代码
MULTI
SET key1 "value1"
LPUSH key1 "value2"    # 错误:对string类型执行list操作
SET key2 "value2"
EXEC
# 返回:1) OK  2) (error) WRONGTYPE  3) OK
# key1和key2都设置成功,LPUSH失败

3.2 一致性(Consistency)

Redis事务可以保证一致性:

  • 入队错误:整个事务不会执行
  • 执行错误:错误命令会被识别并跳过
  • 服务器宕机:根据持久化配置恢复数据

3.3 隔离性(Isolation)

Redis事务的隔离性分析:

隔离级别 是否支持 说明
读未提交 支持 事务执行前可看到其他事务未提交的数据
读已提交 支持 单线程模型,事务执行时不会被干扰
可重复读 不支持 WATCH可以部分实现
串行化 支持 单线程执行

3.4 持久性(Durability)

取决于持久化配置:

  • 无持久化:不满足持久性
  • RDB:可能丢失几分钟数据
  • AOF(everysec):可能丢失1秒数据
  • AOF(always):基本满足持久性

ACID对比表

特性 Redis事务 MySQL事务
原子性 部分(无回滚) 完整支持
一致性 支持 支持
隔离性 单线程隔离 多种隔离级别
持久性 取决于配置 支持(WAL)

四、Lua脚本详解

4.1 为什么需要Lua脚本?

实际场景:分布式锁释放时,需要先判断锁是否属于自己,再执行DEL操作。如果用两个命令执行,中间可能被其他客户端干扰。

Redis Lua脚本的优势:

  1. 原子性:整个脚本作为一个整体执行
  2. 减少网络开销:多个命令一次发送
  3. 复用性:脚本可以缓存,重复使用
  4. 灵活性:实现复杂业务逻辑

4.2 Lua脚本基本使用

执行Lua脚本:

redis 复制代码
# EVAL命令
EVAL "return redis.call('GET', KEYS[1])" 1 user:1:name

# 使用KEYS和ARGV数组
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 user:1:name "zhangsan"

脚本缓存:

redis 复制代码
# 加载脚本,返回SHA1校验和
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 返回:e465c6bf...(SHA1值)

# 使用SHA1执行脚本
EVALSHA e465c6bf... 1 user:1:name

4.3 常用Lua脚本示例

1. 分布式锁释放:

lua 复制代码
-- unlock.lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end
shell 复制代码
# 执行
redis-cli --eval unlock.lua lock_key , my_unique_value

2. 限流脚本:

lua 复制代码
-- rate_limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local current = redis.call("GET", key)
if current and tonumber(current) >= limit then
    return 0  -- 超过限制
end

redis.call("INCR", key)
if tonumber(current) == 0 then
    redis.call("EXPIRE", key, window)
end
return 1  -- 允许访问

3. 库存扣减(防超卖):

lua 复制代码
-- deduct_stock.lua
local stock = tonumber(redis.call("GET", KEYS[1]))
if stock <= 0 then
    return -1  -- 库存不足
end
redis.call("DECR", KEYS[1])
return stock - 1

4.4 Lua脚本注意事项

踩坑提醒:Lua脚本执行时会阻塞Redis,长时间运行的脚本会影响其他请求。

注意事项:

问题 解决方案
脚本执行时间过长 设置lua-time-limit,默认5秒
脚本过大 使用SCRIPT LOAD缓存脚本
随机函数问题 Redis禁用了部分随机函数
数据类型错误 使用tonumber()tostring()转换
conf 复制代码
# redis.conf 配置
lua-time-limit 5000  # Lua脚本最大执行时间(毫秒)

五、事务 vs Lua脚本对比

对比项 Redis事务 Lua脚本
原子性 部分(无回滚) 完整原子性
复杂逻辑 不支持 支持条件判断、循环
网络开销 多次交互 单次交互
错误处理 无法捕获 可以处理错误
使用场景 简单批量操作 复杂原子操作
学习成本 中等

六、踩坑提醒

踩坑提醒:事务不支持回滚

问题:

redis 复制代码
MULTI
SET key1 "value1"
INCR key1        # 对string执行INCR会失败
SET key2 "value2"
EXEC
# key1被设置为"value1",INCR失败,key2被设置为"value2"

解决方案:

  • 使用Lua脚本实现条件判断
  • 在应用层做数据校验

踩坑提醒:WATCH使用后需要UNWATCH

问题:

WATCH监视的key在EXEC/DISCARD后自动取消,但如果事务被取消后想重新使用这些key,需要手动UNWATCH。

redis 复制代码
WATCH key1
GET key1
# 发现值不符合预期,想放弃本次操作
UNWATCH    # 手动取消监视

七、面试高频考点

考点1:Redis事务和MySQL事务的区别?

答案:

对比项 Redis事务 MySQL事务
回滚机制 不支持 支持
隔离级别 无明确级别 四种隔离级别
锁机制 无锁 行锁、表锁等
原子性 部分(无回滚) 完整支持
使用场景 批量命令执行 数据一致性保证

考点2:Redis事务如何实现乐观锁?

答案:

使用WATCH命令监视key,在EXEC执行前检查key是否被修改。如果被修改,事务返回nil,应用层可以重试。

redis 复制代码
WATCH key
val = GET key
MULTI
SET key new_val
EXEC
# 如果key被其他客户端修改,EXEC返回nil

考点3:Lua脚本的优势是什么?

答案:

  1. 原子性:整个脚本原子执行,不会被其他命令打断
  2. 减少网络开销:多个命令一次发送
  3. 复用性:脚本缓存后可重复使用
  4. 灵活性:支持条件判断、循环等复杂逻辑

考点4:如何处理Lua脚本执行超时?

答案:

  1. 设置合理的lua-time-limit配置
  2. 使用SCRIPT KILL命令终止正在执行的脚本
  3. 如果脚本已经执行了写操作,只能使用SHUTDOWN NOSAVE强制关闭
  4. 优化脚本逻辑,避免长时间运行

八、参考资料

  1. Redis官方文档 - Transactions
  2. Redis Lua Scripting

九、互动话题

  1. 你在生产环境中使用Redis事务还是Lua脚本更多?为什么?
  2. 如何设计一个可靠的分布式锁方案?需要考虑哪些问题?
  3. Lua脚本执行超时时,你会如何处理?

欢迎在评论区分享你的经验和见解!


下一期预告:Day8 - Redis发布订阅与消息队列,敬请期待!

相关推荐
流星白龙1 小时前
【MySQL高阶】14.MySQL存储结构
android·数据库·mysql
一只fish1 小时前
Oracle官方文档翻译《Database Concepts 26ai》第18章-进程架构
数据库·oracle
farerboy2 小时前
15-Java while 和 do...while循环
java·后端
流星白龙2 小时前
【MySQL高阶】17.InnoDB 内存结构
数据库·mysql·adb
刘欣的博客2 小时前
LiteNetLib WinForm Demo
数据库·microsoft·c#
Byron__2 小时前
Redis高可用面试知识:持久化+主从复制+哨兵机制
redis·面试·bootstrap
Lyyaoo.2 小时前
【MySQL】索引
数据库·mysql
i220818 Faiz Ul2 小时前
民谣网站|基于Springboot的民谣网站管理系统(源码+数据库+文档)
java·数据库·spring boot·后端·论文·毕设·民谣网站
Oneslide2 小时前
windows cmd输入输出都很卡
后端