面试知识库
中 进阶

Redis事务与Lua脚本#

一句话答案#

Redis 事务(MULTI/EXEC)不支持回滚,Lua 脚本保证原子性且支持逻辑判断,生产推荐 Lua。

核心要点

Redis 事务流程(MULTI/EXEC):

MULTI              # 开启事务,后续命令入队
SET key1 "a"       # QUEUED
SET key2 "b"       # QUEUED
INCR key3          # QUEUED
EXEC               # 一次性执行所有命令,返回每条结果

DISCARD            # 放弃事务(清空队列)
plaintext

WATCH 乐观锁:

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 机制不需要(原子执行)
网络开销多次入队 + EXECEVAL 一次传输
错误处理入队时语法错误 → EXEC 整体拒绝(2.6.5+);执行时运行错误 → 其余命令照常执行,不回滚脚本中途报错 → 已执行的写命令同样不回滚,需先校验再写
生产推荐简单场景✅ 复杂原子操作首选

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: "d8f2fad9f8e86a53d2a6ebd960b33c4972cacc37"
EVALSHA "d8f2fad9..." 1 mykey myvalue
bash

典型 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  -- 库存不足
end
lua

面试回答(2分钟版)

Redis 提供两种保证操作原子性的方式:事务和 Lua 脚本。事务用 MULTI 开启、EXEC 提交,中间的命令会排队一次性执行,但它不支持回滚——如果某条命令执行出错,已执行的命令不会撤销,这和关系型数据库的事务有本质区别。事务还支持 WATCH 做乐观锁,如果被监视的 key 在 EXEC 前被其他客户端修改了,整个事务会取消。Lua 脚本是更推荐的方案,脚本在 Redis 中原子执行,执行期间不会被其他命令打断,而且支持 if/else 条件判断,可以实现更复杂的业务逻辑。典型的使用场景比如分布式锁的加锁解锁、库存扣减的先判断后扣减,都用 Lua 脚本保证原子性。生产中建议用 EVALSHA 代替 EVAL 减少网络传输(Redis 7.0 起还可以用 Redis Functions,FUNCTION LOAD 注册、FCALL 调用,函数会随 RDB/AOF 持久化并复制到从库),同时 Lua 脚本要尽量简短,执行时间过长会阻塞 Redis 主线程。在 Cluster 模式下操作多个 key 时需要用 Hash Tag 确保它们在同一个 slot。

追问与易错

追问方向:

  • “Redis 事务支持回滚吗?”→ 不支持回滚。分两种错误:入队时的语法错误(命令不存在、参数个数错)从 2.6.5 起会让 EXEC 直接返回 EXECABORT、整个事务都不执行;EXEC 后的运行时错误(如对 String 执行 LPOP)只让这一条失败,其余命令照常执行、已执行的不会撤销。官方理由是支持回滚会显著影响 Redis 的简单性和性能
  • “Lua 脚本太长会怎样?”→ Lua 脚本在 Redis 中原子执行期间会阻塞主线程。超过 busy-reply-threshold(7.0 前叫 lua-time-limit,旧名仍可用,默认 5000ms)后 Redis 不会自动终止脚本,只是开始给其他客户端返回 BUSY 错误;此时只能执行 SCRIPT KILL(仅限还没执行过写命令的脚本)或 SHUTDOWN NOSAVE(脚本已写过数据时唯一的办法,会丢掉上次持久化后的数据)。所以生产上脚本必须简短
  • “EVAL 和 EVALSHA 区别?”→ EVAL 每次传输完整脚本文本,网络开销大;EVALSHA 先通过 SCRIPT LOAD 缓存脚本到 Redis 返回 SHA1 摘要,后续只传摘要执行,减少网络传输;生产推荐 EVALSHA。注意脚本缓存不持久化,重启、主从切换后会丢,EVALSHA 返回 NOSCRIPT 时客户端要重新 SCRIPT LOAD(主流客户端会自动处理)

易错点:

  • ❌ Redis 事务和 DB 事务一样——不支持回滚
  • ❌ Lua 脚本可以很复杂——应尽量简短