← 返回题目列表

Redis 事务和 Lua 脚本有什么区别?

高频 中等 第 13 / 36 题 更新于 2026/07/27
Redis事务Lua原子性

简化版

Redis 事务用 MULTI/EXEC 把多个命令排队后一次执行,保证执行期间不会被其他客户端命令插入,但不支持传统数据库那种回滚。Lua 脚本可以把读取、判断、写入等逻辑放到服务端原子执行,更适合复杂原子操作。

详细版

Redis 事务特点:

  • MULTI 开启事务;
  • 后续命令进入队列;
  • EXEC 时按顺序执行;
  • 执行期间不会被其他客户端命令穿插;
  • 命令运行时出错不会自动回滚已经执行的命令;
  • 可用 WATCH 做乐观锁,监控 key 是否被修改。

Lua 脚本特点:

  • 在 Redis 服务端执行;
  • 脚本执行期间具有原子性;
  • 支持读取、判断、循环和条件逻辑;
  • 减少多次网络往返;
  • 脚本过慢会阻塞 Redis。

简单批量命令可用事务,复杂判断和原子组合更适合 Lua。

完整版教学

一、Redis 事务不是关系型数据库事务

很多人把 Redis 事务理解成 MySQL 事务,这是误区。Redis 事务没有隔离级别、undo log 和自动回滚机制。

Redis 事务更像是“命令队列”:先把命令排起来,EXEC 时一次性按顺序执行。执行过程中不会插入其他客户端命令,所以能保证队列执行的连续性。

例如:

MULTI
INCR counter
LPUSH log item
EXEC

这组命令会在 EXEC 时按顺序执行,中间不会穿插其他客户端命令。但如果第二条命令在运行时报错,Redis 不会像 MySQL 那样把第一条已经成功的命令自动回滚。Redis 事务更强调“批量顺序执行和不被插队”,不是 ACID 事务模型。

二、事务为什么不能处理中途逻辑判断

事务里的命令在 EXEC 前只是入队,不会立即返回真实执行结果。因此你不能在事务中写这种逻辑:

GET stock
if stock > 0 then DECR stock

因为 GET 的结果在 EXEC 前拿不到。这个场景要么用 Lua 脚本,要么用 Redis 原生原子命令设计。

扣库存就是典型例子。你希望“先读库存,如果大于 0 再扣减”,这需要根据读取结果做判断;MULTI/EXECGET 在执行前只返回 queued,客户端没法在事务内部根据真实库存决定是否继续。Lua 脚本则可以在服务端完成读取、判断和扣减。

能力MULTI/EXECLua
多命令顺序执行支持支持
执行中条件判断
自动回滚不支持不支持传统回滚
原子读改写需要 WATCH 或命令设计适合

记忆钩子:Redis 事务像排队执行命令,Lua 像把判断逻辑搬到 Redis 里一次执行完。

三、WATCH 是乐观锁

WATCH key 可以监控某些 key。如果从 WATCHEXEC 之间,这些 key 被其他客户端修改,EXEC 会失败。客户端可以重试。

它适合简单的 CAS 场景,但高并发热点 key 下可能频繁失败重试,吞吐不一定好。

时间线如下:

client A: WATCH stock
client A: GET stock -> 10
client B: DECR stock -> 9
client A: MULTI
client A: SET stock 9
client A: EXEC -> 失败,因为 stock 被改过

WATCH 可以避免覆盖别人更新,但失败后要客户端重新读取和重试。热点 key 下大量客户端同时 WATCH,可能反复失败,通常不如 Lua 脚本或原生命令更稳定。

四、Lua 适合复杂原子操作

Lua 脚本在 Redis 内部执行,执行期间不会被其他命令打断。它能把读、判断、写组合成一个原子逻辑。

比如扣库存:

local stock = tonumber(redis.call("get", KEYS[1]))
if stock and stock > 0 then
  return redis.call("decr", KEYS[1])
else
  return -1
end

这比客户端先 GETDECR 安全,因为中间不会被其他客户端插入修改。

Lua 还能减少网络往返。客户端原本要 GET、判断、DECR、写日志,可能需要多次请求;脚本一次发送到 Redis,服务端执行完返回结果。高并发下,减少往返和避免并发穿插都很重要。Redis 7 还支持 Functions,但面试基础题讲 Lua 的原子执行和边界通常已经足够。

五、Lua 脚本也要控制复杂度

Lua 原子性的代价是阻塞。脚本执行期间,Redis 不能执行其他命令。如果脚本里遍历大集合、死循环或做复杂计算,会拖慢整个实例。

生产脚本要短小、确定、可控,避免处理海量数据。Redis Cluster 下还要注意脚本涉及的 key 通常需要落在同一个 hash slot。

例如脚本遍历一个 100 万元素的 Set,再做复杂过滤,即使逻辑原子,也会长时间占住 Redis 主线程。Redis 适合短脚本封装原子逻辑,不适合把大量业务计算塞进 Lua。脚本参数也要明确声明 key,Cluster 才能判断槽位和路由。

六、什么时候选事务,什么时候选 Lua

简单批量写、无需中途判断,可以用 MULTI/EXEC;需要 CAS,可以用 WATCH,但要接受失败重试;需要读、判断、写一体化并发安全,优先 Lua 或 Redis 原生命令。能用 INCRDECRSET NX PXHINCRBY 这类原子命令时,通常不需要 Lua。

选择路径:
单命令能表达 -> 原子命令
多命令无判断 -> MULTI/EXEC
乐观 CAS -> WATCH + EXEC
读判断写组合 -> Lua

七、常见误区与追问

  • 误区:Redis 事务等同于 MySQL 事务。 Redis 事务没有隔离级别和 undo 回滚,更像命令排队后连续执行。
  • 误区:MULTI 中可以根据 GET 结果写 if 判断。 命令在 EXEC 前只是入队,真实结果要到 EXEC 才返回。
  • 误区:Redis 事务运行时出错会自动回滚。 已执行成功的命令不会因为后续命令错误自动撤销。
  • 误区:Lua 越复杂越能减少客户端逻辑。 脚本执行期间阻塞 Redis,生产脚本应短小可控,不能遍历大 key 或做重计算。
  • 追问:WATCH 为什么叫乐观锁? 它不阻塞别人修改,而是在 EXEC 时检查被监控 key 是否变化,变化则事务失败并由客户端重试。
  • 追问:Redis Cluster 下 Lua 有什么限制? 脚本涉及的多个 key 通常需要在同一个 hash slot,否则单节点无法原子处理跨槽数据。

八、加强记忆

Redis 事务是 MULTI/EXEC 的命令排队和顺序执行,保证执行期间不被其他命令插入,但不提供 MySQL 那种自动回滚和隔离级别;WATCH 是乐观锁,冲突时让 EXEC 失败并由客户端重试。Lua 脚本把读取、判断、写入放到服务端原子执行,适合扣库存、解锁校验这类复合逻辑。选型时先看原生命令能不能表达,再看是否需要中途判断;Lua 要短小可控,Cluster 下还要注意同槽限制。