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

文章目录
-
- 写在前面
- 一、Redis事务基础
-
- [1.1 什么是Redis事务?](#1.1 什么是Redis事务?)
- [1.2 事务相关命令](#1.2 事务相关命令)
- [1.3 事务基本使用](#1.3 事务基本使用)
- 二、WATCH命令详解
-
- [2.1 WATCH的作用](#2.1 WATCH的作用)
- [2.2 WATCH使用示例](#2.2 WATCH使用示例)
- [2.3 WATCH实现乐观锁](#2.3 WATCH实现乐观锁)
- 三、Redis事务的ACID分析
-
- [3.1 原子性(Atomicity)](#3.1 原子性(Atomicity))
- [3.2 一致性(Consistency)](#3.2 一致性(Consistency))
- [3.3 隔离性(Isolation)](#3.3 隔离性(Isolation))
- [3.4 持久性(Durability)](#3.4 持久性(Durability))
- ACID对比表
- 四、Lua脚本详解
-
- [4.1 为什么需要Lua脚本?](#4.1 为什么需要Lua脚本?)
- [4.2 Lua脚本基本使用](#4.2 Lua脚本基本使用)
- [4.3 常用Lua脚本示例](#4.3 常用Lua脚本示例)
- [4.4 Lua脚本注意事项](#4.4 Lua脚本注意事项)
- [五、事务 vs Lua脚本对比](#五、事务 vs 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脚本的优势:
- 原子性:整个脚本作为一个整体执行
- 减少网络开销:多个命令一次发送
- 复用性:脚本可以缓存,重复使用
- 灵活性:实现复杂业务逻辑
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脚本的优势是什么?
答案:
- 原子性:整个脚本原子执行,不会被其他命令打断
- 减少网络开销:多个命令一次发送
- 复用性:脚本缓存后可重复使用
- 灵活性:支持条件判断、循环等复杂逻辑
考点4:如何处理Lua脚本执行超时?
答案:
- 设置合理的
lua-time-limit配置 - 使用
SCRIPT KILL命令终止正在执行的脚本 - 如果脚本已经执行了写操作,只能使用
SHUTDOWN NOSAVE强制关闭 - 优化脚本逻辑,避免长时间运行
八、参考资料
九、互动话题
- 你在生产环境中使用Redis事务还是Lua脚本更多?为什么?
- 如何设计一个可靠的分布式锁方案?需要考虑哪些问题?
- Lua脚本执行超时时,你会如何处理?
欢迎在评论区分享你的经验和见解!
下一期预告:Day8 - Redis发布订阅与消息队列,敬请期待!