少女祈祷中...
NEKO
知识库

redis 事务 lua 面试要点

一、Redis 事务基础类

Q1:Redis 事务是什么?

  • 一组命令的集合,用 MULTI 开启,EXEC 执行。

  • 命令入队,EXEC 时顺序、一次性执行。

  • 执行期间不会被其他客户端命令打断。

  • 但不支持回滚,是“弱事务”。

Q2:Redis 事务的命令有哪些?

命令

作用

MULTI

开启事务

EXEC

执行事务

DISCARD

放弃事务

WATCH

监视 key

UNWATCH

取消监视

Q3:Redis 事务的执行流程?

  1. MULTI 开启事务。

  2. 命令入队,返回 QUEUED。

  3. EXEC 执行所有入队命令。

  4. 返回结果数组,每个命令一个结果。

  5. DISCARD 可放弃,清空队列。

Q4:Redis 事务支持回滚吗?

  • 不支持。

  • 如果某条命令运行时出错,其他命令仍会执行。

  • 只有入队时语法错误,EXEC 才整体失败。

  • 所以 Redis 事务不保证原子性(指回滚意义上的)。

Q5:Redis 事务和数据库事务的区别?

维度

Redis

数据库

原子性

不保证回滚

保证

隔离性

单线程天然隔离

靠隔离级别

持久性

取决于持久化

保证

一致性

弱

强

回滚

不支持

支持

Q6:Redis 事务的错误分几种?

  1. 入队错误:命令不存在或语法错,EXEC 时整体失败。

  2. 执行错误:如对 String 执行 LPUSH,该命令失败,其他继续。

  3. Redis 不因执行错误回滚。

Q7:为什么 Redis 事务不支持回滚?

  • 设计哲学:错误是编程错误,应在开发时发现。

  • 回滚需要额外记录和开销,影响性能。

  • 单线程执行,不存在并发导致的中间状态。

  • 所以选择简单高效,不回滚。

二、WATCH 与乐观锁类

Q8:WATCH 是什么?

  • 乐观锁机制。

  • WATCH key 后,如果 key 在 EXEC 前被修改,事务失败。

  • EXEC 返回 nil,需重试。

  • 用于实现 CAS(Check-And-Set)。

Q9:WATCH 的工作原理?

  1. WATCH key 记录 key 的版本。

  2. 其他客户端修改 key,标记该 key 被修改。

  3. EXEC 时检查被 WATCH 的 key 是否被改。

  4. 被改则事务不执行,返回 nil。

Q10:WATCH 怎么实现乐观锁?

WATCH stock
val = GET stock
if val > 0:
    MULTI
    DECR stock
    EXEC
else:
    UNWATCH
  • 如果 stock 被改,EXEC 失败,重试。

  • 适合冲突少的场景。

Q11:WATCH 和悲观锁的区别?

维度

WATCH 乐观锁

悲观锁

加锁

不加锁

加锁

冲突

失败重试

阻塞等待

适用

冲突少

冲突多

性能

高

低

Q12:WATCH 的 key 在 EXEC 前过期会怎样?

  • 视为被修改,事务失败。

  • 因为过期也是一种变更。

  • 需重试。

Q13:UNWATCH 什么时候用?

  • 放弃事务前取消监视。

  • EXEC 或 DISCARD 后自动 UNWATCH。

  • 手动 UNWATCH 可提前释放。

三、Lua 脚本类

Q14:Redis 为什么引入 Lua?

  • 事务不支持回滚,复杂逻辑难保证原子。

  • Lua 脚本在 Redis 中原子执行,不被其他命令打断。

  • 可封装复杂逻辑,减少网络往返。

  • 适合 CAS、限流、分布式锁等。

Q15:Lua 脚本的原子性怎么保证?

  • Redis 单线程执行命令。

  • Lua 脚本作为一个整体执行,期间不处理其他命令。

  • 所以脚本内所有操作原子。

  • 但脚本执行时间长会阻塞其他请求。

Q16:Lua 脚本怎么执行?

  • EVAL script numkeys key... arg...

  • EVALSHA sha1 numkeys key... arg...:用缓存脚本。

  • SCRIPT LOAD:预加载脚本,返回 SHA1。

  • SCRIPT EXISTS、SCRIPT FLUSH:管理脚本缓存。

Q17:EVAL 和 EVALSHA 的区别?

  • EVAL:每次传完整脚本,网络开销大。

  • EVALSHA:传 SHA1,脚本已缓存,开销小。

  • 生产用 SCRIPT LOAD + EVALSHA。

  • 脚本不存在时回退 EVAL。

Q18:Lua 脚本能保证原子性吗?

  • 能,脚本执行期间不被打断。

  • 但不保证“要么全成功要么全失败”。

  • 脚本内错误不会自动回滚已执行的命令。

  • 原子性指“不被中断”,不是“可回滚”。

Q19:Lua 脚本有什么注意事项?

  • 脚本要短,避免阻塞。

  • 不要用随机命令(除非固定 seed)。

  • 不要用阻塞命令。

  • key 要显式传,不要硬编码。

  • 集群下所有 key 要同槽。

Q20:Lua 脚本会阻塞 Redis 吗?

  • 会。脚本执行期间单线程被占用。

  • 长脚本阻塞所有请求。

  • 要控制执行时间,避免死循环。

  • lua-time-limit 可设超时,但不会自动杀。

Q21:Lua 脚本怎么访问 Redis 数据?

  • redis.call('get', KEYS[1]):执行命令,出错抛异常。

  • redis.pcall(...):执行命令,出错返回错误表。

  • KEYS、ARGV 是传入的 key 和参数。

Q22:Lua 脚本能做分布式锁吗?

  • 能。删锁的校验 + 删除用 Lua 保证原子。

  • 例:

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
  • 这是 Lua 最经典的应用。

四、Pipeline 与事务对比类

Q23:Pipeline 和事务的区别?

维度

Pipeline

事务

目的

减少 RTT

原子执行

原子性

无

有(顺序执行)

命令间

可插入其他

不插入

错误

逐条

整体/逐条

实现

客户端打包

MULTI/EXEC

Q24:Pipeline 是原子的吗?

  • 不是。命令之间可能插入其他客户端命令。

  • 只是批量发送,减少网络往返。

  • 要原子用事务或 Lua。

Q25:事务和 Lua 怎么选?

  • 简单命令组合:事务。

  • 复杂逻辑、需原子:Lua。

  • 需条件判断:Lua。

  • 需回滚:都不支持,用数据库。

Q26:Pipeline + 事务能一起用吗?

  • 能。MULTI、命令、EXEC 一起打包发送。

  • 减少 RTT,同时保证事务。

  • 但注意批量大小。

Q27:Lua 和事务的原子性区别?

  • 事务:命令入队,EXEC 时顺序执行,不中断。

  • Lua:脚本整体执行,不中断,可写复杂逻辑。

  • 两者都不支持回滚。

  • Lua 更灵活,事务更简单。

五、实战与陷阱类

Q28:事务中命令入队后能改吗?

  • 不能。入队后只能 EXEC 或 DISCARD。

  • 想改要 DISCARD 重新来。

  • 或用 WATCH + 重试。

Q29:WATCH 的 key 很多会怎样?

  • 记录多个 key 版本,开销增加。

  • EXEC 时检查所有 key。

  • 尽量少 WATCH。

Q30:Lua 脚本中能用随机数吗?

  • 能,但要用固定 seed,保证可重现。

  • Redis 5+ 要求脚本确定性。

  • 否则主从/AOF 可能不一致。

Q31:Lua 脚本在集群下有什么限制?

  • 所有 key 必须在同一槽。

  • 用 hash tag {} 保证同槽。

  • 不能跨槽访问。

Q32:脚本缓存会占内存吗?

  • 会。脚本存在内存中。

  • 用 SCRIPT FLUSH 清理。

  • 大量不同脚本要管理。

Q33:Lua 脚本超时了怎么办?

  • lua-time-limit 默认 5 秒。

  • 超时后 Redis 记录日志,但不自动杀。

  • 其他客户端收到 BUSY 错误。

  • 只能 SCRIPT KILL(未写)或 SHUTDOWN NOSAVE。

Q34:Redis 事务能保证隔离性吗?

  • 能。单线程执行,事务期间不执行其他命令。

  • 天然串行隔离。

  • 但 WATCH 失败是乐观锁,不是隔离级别。

Q35:为什么 Lua 脚本要显式传 key?

  • 集群下 Redis 要知道 key 路由到哪个槽。

  • 显式传 key 才能正确路由。

  • 不要从 ARGV 拼 key。

评论