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/EXEC 里 GET 在执行前只返回 queued,客户端没法在事务内部根据真实库存决定是否继续。Lua 脚本则可以在服务端完成读取、判断和扣减。
| 能力 | MULTI/EXEC | Lua |
|---|---|---|
| 多命令顺序执行 | 支持 | 支持 |
| 执行中条件判断 | 弱 | 强 |
| 自动回滚 | 不支持 | 不支持传统回滚 |
| 原子读改写 | 需要 WATCH 或命令设计 | 适合 |
记忆钩子:Redis 事务像排队执行命令,Lua 像把判断逻辑搬到 Redis 里一次执行完。
三、WATCH 是乐观锁
WATCH key 可以监控某些 key。如果从 WATCH 到 EXEC 之间,这些 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
这比客户端先 GET 再 DECR 安全,因为中间不会被其他客户端插入修改。
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 原生命令。能用 INCR、DECR、SET NX PX、HINCRBY 这类原子命令时,通常不需要 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 下还要注意同槽限制。