📋
概念概览
📦
内置机制
事务(Transaction)
MULTI / EXEC 命令组
Redis 原生支持的批量命令执行机制。通过 MULTI 开启事务,将命令依次入队,最终通过 EXEC 一次性批量执行,保证这批命令不会被其他客户端打断。
- ✓ 原子性(部分):命令整体执行,不会被插队;但单条命令出错不回滚
- ✓ 简单易用:无需编写脚本,支持所有 Redis 命令
- ✗ 不支持回滚:运行时错误不回滚,只跳过出错命令
- ✗ 不支持条件逻辑:无法根据前一条命令的结果决定后续操作
🌙
脚本引擎
Lua 脚本
EVAL / EVALSHA 命令
通过 EVAL 将 Lua 脚本发送到 Redis 服务端执行。脚本在服务端单线程原子运行,可调用 redis.call() 执行任意 Redis 命令,并根据结果编写条件/循环逻辑。
- ✓ 真正的原子性:脚本执行期间不会切换到其他命令
- ✓ 支持复杂逻辑:条件判断、循环、错误处理均可实现
- ✓ 网络开销小:一次 RTT 传输,减少多次网络往返
- ✗ 调试难度高:脚本报错信息较简略,排查成本高
🔄
执行流程对比
📦 事务执行流程
① 客户端发送 MULTI
↓
② 发送命令 SET key1 val1
(命令入队,暂不执行)
(命令入队,暂不执行)
↓
③ 发送命令 SET key2 val2
(继续入队)
(继续入队)
↓
④ 客户端发送 EXEC
↓
⑤ Redis 顺序执行所有队列命令
(原子执行,不插队)
(原子执行,不插队)
↓
⑥ 返回每条命令的执行结果数组
⚠️ 共 N+3 次网络往返(MULTI、N条命令、EXEC)
🌙 Lua脚本执行流程
① 客户端编写完整 Lua 脚本
(含逻辑判断)
(含逻辑判断)
↓
② 发送 EVAL script numkeys keys args
(一次网络请求)
(一次网络请求)
↓
③ Redis 服务端加载 Lua 脚本
↓
④ 单线程原子执行脚本
(可调用 redis.call,有条件逻辑)
(可调用 redis.call,有条件逻辑)
↓
⑤ 返回脚本最终结果
✅ 仅 1 次网络往返,效率更高
📊
核心维度对比
| 维度 | 📦 事务(Transaction) | 🌙 Lua 脚本 |
|---|---|---|
| 原子性 | 部分原子 整体不被插队,但单条出错不回滚 | 完全原子 脚本执行期间服务端不处理其他命令 |
| 逻辑复杂度 | 简单 仅支持顺序执行,无条件判断 | 复杂 支持 if/for/while 等完整编程逻辑 |
| 网络开销 | 大 多次网络往返(至少 N+2 次 RTT) | 小 一次网络传输(1 次 RTT) |
| 回滚支持 | 不支持 运行时错误仅跳过,不回滚 | 受限支持 可用 pcall 捕获错误并中止 |
| 读-改-写 | 不支持 不能基于上条结果决定下条命令 | 支持 可读取值后在脚本内做逻辑判断 |
| 调试难度 | 低 命令直观,报错清晰 | 高 Lua 错误信息较少,排查困难 |
| 长时阻塞风险 | 低 命令数有限,执行快 | 存在 脚本有死循环风险,会阻塞整个服务 |
| 典型命令 | MULTI / EXEC / DISCARD / WATCH |
EVAL / EVALSHA / SCRIPT LOAD |
💻
代码示例
📦 事务:批量更新两个 key
# 开启事务 MULTI # OK # 命令入队(暂不执行) SET user:1:name "Alice" # QUEUED SET user:1:age 25 # QUEUED INCR user:count # QUEUED # 批量执行 EXEC # 1) OK # 2) OK # 3) (integer) 1 # ⚠️ 若某条命令类型错误, # 只跳过该命令,其余照常执行
🌙 Lua:扣减库存 + 记录日志(原子)
-- Lua 脚本(在 EVAL 中传入) local stock_key = KEYS[1] local log_key = KEYS[2] local amount = tonumber(ARGV[1]) -- 读取当前库存 local stock = tonumber( redis.call('GET', stock_key) or 0 ) -- 条件判断:库存不足则返回 -1 if stock < amount then return -1 end -- 原子扣减并记录日志 redis.call('DECRBY', stock_key, amount) redis.call('RPUSH', log_key, 'deduct:' .. amount) return stock - amount -- 客户端调用: -- EVAL script 2 stock:101 log:101 5
🎯
适用场景
📦 选择事务的场景
1
批量写入多个简单 key,无需读取中间结果(如初始化配置批量 SET)
2
需要乐观锁(WATCH):监听某个 key 是否在事务期间被修改
3
逻辑简单、团队 Lua 技能不足、快速实现批量操作
4
对网络延迟不敏感,可以接受多次 RTT
🌙 选择 Lua 脚本的场景
1
分布式锁:SET NX + 校验 + DEL 需要原子完成,防止误删他人锁
2
扣减库存:先读库存,判断充足,再扣减并记日志,整体原子
3
限流计数器:读取计数 → 判断阈值 → 更新 → 设过期,CAS 原子
4
排行榜批操作:读分数 → 加权计算 → 更新多个结构,需要一致性
🧭 决策指南:我该选哪个?
📦 选事务,当…
- 只需批量执行,无需读取中间结果
- 命令之间相互独立,无条件依赖
- 需要乐观锁 WATCH 监听 key 变化
- 追求简单、团队熟悉度高
🌙 选 Lua,当…
- 需要「读取 → 判断 → 写入」的原子操作
- 操作涉及条件分支或循环
- 网络延迟敏感,希望减少 RTT
- 实现分布式锁、限流等高并发场景
💡 一句话总结
事务适合批量简单写操作(如 SET a 1; SET b 2),逻辑清晰但不支持回滚与条件判断;
Lua 脚本适合需要原子性的复杂操作(如分布式锁、扣库存+记日志),
在服务端一次性执行,减少网络开销并支持完整的编程逻辑。
核心判断标准:操作之间是否有数据依赖? 有 → 用 Lua;无 → 事务足够。