中 进阶
Redis事务与Lua脚本#
一句话答案#
Redis 事务(MULTI/EXEC)不支持回滚,Lua 脚本保证原子性且支持逻辑判断,生产推荐 Lua。
核心要点
Redis 事务流程(MULTI/EXEC):
MULTI # 开启事务,后续命令入队
SET key1 "a" # QUEUED
SET key2 "b" # QUEUED
INCR key3 # QUEUED
EXEC # 一次性执行所有命令,返回每条结果
DISCARD # 放弃事务(清空队列)plaintextWATCH 乐观锁:
WATCH stock:1001 # 监视 key
val = GET stock:1001 # 读取当前值
MULTI
DECRBY stock:1001 1 # 扣减库存
EXEC # 如果 stock:1001 在 WATCH 后被其他客户端修改 → EXEC 返回 nil(事务取消)plaintext事务 vs Lua 对比:
| 特性 | MULTI/EXEC 事务 | Lua 脚本 |
|---|---|---|
| 原子性 | ❌ 不支持回滚,出错继续执行 | ✅ 原子执行,中途不被打断 |
| 条件逻辑 | ❌ 无法 if/else | ✅ 支持完整逻辑控制 |
| 乐观锁 | ✅ WATCH 机制 | 不需要(原子执行) |
| 网络开销 | 多次入队 + EXEC | EVAL 一次传输 |
| 错误处理 | 命令错误不回滚已执行的 | 脚本报错则整体不生效 |
| 生产推荐 | 简单场景 | ✅ 复杂原子操作首选 |
Lua 脚本使用:
# EVAL 直接执行(每次传输脚本全文)
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
# EVALSHA 按摘要执行(先 SCRIPT LOAD 缓存脚本,减少网络传输)
SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])"
# 返回 SHA1: "a42059b356c875f0717db19a51f6aaa9161571a2"
EVALSHA "a42059..." 1 mykey myvaluebash典型 Lua 场景 — 库存扣减(先判断后扣减,原子执行):
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1 -- 扣减成功
else
return 0 -- 库存不足
endlua面试回答(2分钟版)
Redis 提供两种保证操作原子性的方式:事务和 Lua 脚本。事务用 MULTI 开启、EXEC 提交,中间的命令会排队一次性执行,但它不支持回滚——如果某条命令执行出错,已执行的命令不会撤销,这和关系型数据库的事务有本质区别。事务还支持 WATCH 做乐观锁,如果被监视的 key 在 EXEC 前被其他客户端修改了,整个事务会取消。Lua 脚本是更推荐的方案,脚本在 Redis 中原子执行,执行期间不会被其他命令打断,而且支持 if/else 条件判断,可以实现更复杂的业务逻辑。典型的使用场景比如分布式锁的加锁解锁、库存扣减的先判断后扣减,都用 Lua 脚本保证原子性。生产中建议用 EVALSHA 代替 EVAL 减少网络传输,同时 Lua 脚本要尽量简短,执行时间过长会阻塞 Redis 主线程。在 Cluster 模式下操作多个 key 时需要用 Hash Tag 确保它们在同一个 slot。
追问与易错
追问方向:
- “Redis 事务支持回滚吗?”→ 不支持回滚,EXEC 执行后如果某条命令出错(运行时错误),已执行的命令不会撤销;Redis 认为命令错误是编程 Bug 不应由数据库兜底,因此设计上不支持回滚
- “Lua 脚本太长会怎样?”→ Lua 脚本在 Redis 中原子执行期间会阻塞主线程,脚本执行时间过长会导致其他客户端请求超时;生产上应保持脚本简短,默认 lua-time-limit=5000ms 超时后可用 SCRIPT KILL 终止
- “EVAL 和 EVALSHA 区别?”→ EVAL 每次传输完整脚本文本,网络开销大;EVALSHA 先通过 SCRIPT LOAD 缓存脚本到 Redis 返回 SHA1 摘要,后续只传摘要执行,减少网络传输;生产推荐 EVALSHA
易错点:
- ❌ Redis 事务和 DB 事务一样——不支持回滚
- ❌ Lua 脚本可以很复杂——应尽量简短