【Redis】事务与Lua脚本Day7(2026年)
写在前面
Redis的事务机制与关系型数据库有很大不同,它没有完整的ACID特性,这让很多开发者感到困惑。同时,Lua脚本作为Redis的原子执行能力,在复杂业务场景中发挥着重要作用。今天我们深入探讨Redis事务和Lua脚本的原理与应用。
文章目录
- 写在前面
- 一、Redis事务基础
- 1.1 什么是Redis事务?
- 1.2 事务相关命令
- 1.3 事务基本使用
- 二、WATCH命令详解
- 2.1 WATCH的作用
- 2.2 WATCH使用示例
- 2.3 WATCH实现乐观锁
- 三、Redis事务的ACID分析
- 3.1 原子性(Atomicity)
- 3.2 一致性(Consistency)
- 3.3 隔离性(Isolation)
- 3.4 持久性(Durability)
- ACID对比表
- 四、Lua脚本详解
- 4.1 为什么需要Lua脚本?
- 4.2 Lua脚本基本使用
- 4.3 常用Lua脚本示例
- 4.4 Lua脚本注意事项
- 五、事务 vs Lua脚本对比
- 六、踩坑提醒
- 七、面试高频考点
- 考点1:Redis事务和MySQL事务的区别?
- 考点2:Redis事务如何实现乐观锁?
- 考点3:Lua脚本的优势是什么?
- 考点4:如何处理Lua脚本执行超时?
- 八、参考资料
- 九、互动话题
一、Redis事务基础
1.1 什么是Redis事务?
实际场景:电商秒杀场景中,需要先检查库存、再扣减库存、最后记录订单,这三个操作必须作为一个整体执行,否则可能出现超卖问题。
Redis事务是一组命令的集合,这些命令会被顺序执行,执行过程中不会被其他客户端的命令打断。但需要注意的是,Redis事务不支持回滚,这与MySQL等关系型数据库有本质区别。
1.2 事务相关命令
| 命令 | 说明 |
|---|---|
MULTI | 开启事务 |
EXEC | 执行事务中的所有命令 |
DISCARD | 取消事务 |
WATCH | 监视一个或多个key |
1.3 事务基本使用
正常执行流程:
# 开启事务 MULTI # 返回:QUEUED # 命令入队 SET user:1:name "zhangsan" QUEUED SET user:1:age 25 QUEUED INCR counter QUEUED # 执行事务 EXEC # 返回:1) OK 2) OK 3) 1取消事务:
MULTI SET key1 "value1" DISCARD # 事务取消,命令不会执行二、WATCH命令详解
2.1 WATCH的作用
经验之谈:WATCH是实现乐观锁的关键,它可以让事务在执行前检查key是否被修改。
WATCH命令用于监视一个或多个key,如果在事务执行前这些key被其他客户端修改,则事务会被拒绝执行。
2.2 WATCH使用示例
场景:转账操作
# 客户端A:监视账户余额 WATCH account:balance GET account:balance # 返回:100 MULTI DECRBY account:balance 50 EXEC # 如果期间没有其他客户端修改balance,事务成功执行如果期间被其他客户端修改:
# 客户端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事务的原子性分析:
| 情况 | 原子性 | 说明 |
|---|---|---|
| 命令入队失败 | 满足 | 整个事务被拒绝执行 |
| 命令执行失败 | 不满足 | 错误命令跳过,其他继续执行 |
| 服务器宕机 | 不满足 | 已执行的命令无法回滚 |
示例:命令执行失败不影响其他命令
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脚本的优势:
- 原子性:整个脚本作为一个整体执行
- 减少网络开销:多个命令一次发送
- 复用性:脚本可以缓存,重复使用
- 灵活性:实现复杂业务逻辑
4.2 Lua脚本基本使用
执行Lua脚本:
# 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"脚本缓存:
# 加载脚本,返回SHA1校验和 SCRIPT LOAD "return redis.call('GET', KEYS[1])" # 返回:e465c6bf...(SHA1值) # 使用SHA1执行脚本 EVALSHA e465c6bf... 1 user:1:name4.3 常用Lua脚本示例
1. 分布式锁释放:
-- unlock.luaifredis.call("GET",KEYS[1])==ARGV[1]thenreturnredis.call("DEL",KEYS[1])elsereturn0end# 执行redis-cli--evalunlock.lua lock_key , my_unique_value2. 限流脚本:
-- rate_limit.lualocalkey=KEYS[1]locallimit=tonumber(ARGV[1])localwindow=tonumber(ARGV[2])localcurrent=redis.call("GET",key)ifcurrentandtonumber(current)>=limitthenreturn0-- 超过限制endredis.call("INCR",key)iftonumber(current)==0thenredis.call("EXPIRE",key,window)endreturn1-- 允许访问3. 库存扣减(防超卖):
-- deduct_stock.lualocalstock=tonumber(redis.call("GET",KEYS[1]))ifstock<=0thenreturn-1-- 库存不足endredis.call("DECR",KEYS[1])returnstock-14.4 Lua脚本注意事项
踩坑提醒:Lua脚本执行时会阻塞Redis,长时间运行的脚本会影响其他请求。
注意事项:
| 问题 | 解决方案 |
|---|---|
| 脚本执行时间过长 | 设置lua-time-limit,默认5秒 |
| 脚本过大 | 使用SCRIPT LOAD缓存脚本 |
| 随机函数问题 | Redis禁用了部分随机函数 |
| 数据类型错误 | 使用tonumber()、tostring()转换 |
# redis.conf 配置 lua-time-limit 5000 # Lua脚本最大执行时间(毫秒)五、事务 vs Lua脚本对比
| 对比项 | Redis事务 | Lua脚本 |
|---|---|---|
| 原子性 | 部分(无回滚) | 完整原子性 |
| 复杂逻辑 | 不支持 | 支持条件判断、循环 |
| 网络开销 | 多次交互 | 单次交互 |
| 错误处理 | 无法捕获 | 可以处理错误 |
| 使用场景 | 简单批量操作 | 复杂原子操作 |
| 学习成本 | 低 | 中等 |
六、踩坑提醒
踩坑提醒:事务不支持回滚
问题:
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。
WATCH key1 GET key1 # 发现值不符合预期,想放弃本次操作 UNWATCH # 手动取消监视七、面试高频考点
考点1:Redis事务和MySQL事务的区别?
答案:
| 对比项 | Redis事务 | MySQL事务 |
|---|---|---|
| 回滚机制 | 不支持 | 支持 |
| 隔离级别 | 无明确级别 | 四种隔离级别 |
| 锁机制 | 无锁 | 行锁、表锁等 |
| 原子性 | 部分(无回滚) | 完整支持 |
| 使用场景 | 批量命令执行 | 数据一致性保证 |
考点2:Redis事务如何实现乐观锁?
答案:
使用WATCH命令监视key,在EXEC执行前检查key是否被修改。如果被修改,事务返回nil,应用层可以重试。
WATCH key val = GET key MULTI SET key new_val EXEC # 如果key被其他客户端修改,EXEC返回nil考点3:Lua脚本的优势是什么?
答案:
- 原子性:整个脚本原子执行,不会被其他命令打断
- 减少网络开销:多个命令一次发送
- 复用性:脚本缓存后可重复使用
- 灵活性:支持条件判断、循环等复杂逻辑
考点4:如何处理Lua脚本执行超时?
答案:
- 设置合理的
lua-time-limit配置 - 使用
SCRIPT KILL命令终止正在执行的脚本 - 如果脚本已经执行了写操作,只能使用
SHUTDOWN NOSAVE强制关闭 - 优化脚本逻辑,避免长时间运行
八、参考资料
- Redis官方文档 - Transactions
- Redis Lua Scripting
九、互动话题
- 你在生产环境中使用Redis事务还是Lua脚本更多?为什么?
- 如何设计一个可靠的分布式锁方案?需要考虑哪些问题?
- Lua脚本执行超时时,你会如何处理?
欢迎在评论区分享你的经验和见解!
下一期预告:Day8 - Redis发布订阅与消息队列,敬请期待!
