🔴 Redis 事务 vs Lua 脚本

原子性保证的两种方式 · 深度对比与适用场景

面试题 8 · 高频考点
📋
概念概览
📦

事务(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,有条件逻辑)
⑤ 返回脚本最终结果
✅ 仅 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;无 → 事务足够。