事务与 Lua
Redis 事务和关系库事务是两回事:没有回滚,没有持久性保证。面试主线:MULTI/EXEC 能保证什么、WATCH 怎么用、Lua 为什么更强。
MULTI/EXEC:弱事务
流程:MULTI 开始入队,后续命令只入队不执行,EXEC 时按序执行。
能保证的:
- 原子性(隔离性):EXEC 期间其他客户端命令不会插入,命令串行执行
- 批量:一次往返执行多条命令
不能保证的:
- 不回滚:运行期某条命令出错(如对 string 做 LPUSH),其他命令照常执行,出错的不回滚
- 不持久:Redis 宕机,事务结果可能丢失(取决于持久化配置)
- 入队错误:EXEC 前发现语法错误,整批不执行
WATCH:乐观锁
WATCH 解决“读后写”的竞态(如扣库存:先读余量再减):
WATCH stock
GET stock -- 读
MULTI
DECR stock -- 写
EXEC -- 若 stock 在 WATCH 后被改过, 返回 nil, 事务不执行- WATCH 的 key 在事务执行前被其他客户端修改,EXEC 返回空,事务整体放弃
- 放弃后要重试(重新 WATCH 再走一遍),这是乐观锁的标准姿势
- 适合竞争不激烈的场景;竞争激烈时重试率高
Lua:更强的原子执行
EVAL 执行 Lua 脚本,整个脚本原子执行,期间其他命令不插入:
| 维度 | MULTI/EXEC | Lua |
|---|---|---|
| 原子性 | 串行执行 | 整个脚本原子 |
| 逻辑能力 | 无(纯命令队列) | 可编程(判断、循环) |
| 回滚 | 无 | 无(错误即停,已执行的不可撤销) |
| 典型场景 | 批量操作 | 复杂原子操作:扣库存、限流 |
Lua 的核心价值:把“读判断写”变成一步原子操作。经典案例:
-- 扣库存: 余量足够才减
local stock = redis.call("GET", KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
return redis.call("DECRBY", KEYS[1], ARGV[1])
end
return -1注意点:脚本要短(长脚本阻塞实例);脚本内禁止随机性和依赖时间的写法(主从复制一致性问题);Redis 7 的 Function 是 Lua 的服务端管理版。
面试追问
- Redis 事务和 MySQL 事务的区别? 无回滚、无持久性保证,只有隔离性(串行执行)。出错不撤销已执行的命令
- WATCH 怎么用? 读前 WATCH,EXEC 时发现 key 被改就放弃事务,客户端重试。乐观锁
- Lua 为什么比事务强? 可编程的原子执行。事务只是命令队列,Lua 能做判断和循环
- 扣库存怎么保证不超卖? Lua 脚本原子完成“检查余量再扣减”。MULTI/EXEC 做不了判断
- Lua 脚本有什么坑? 长脚本阻塞实例;不要用随机值(主从复制会不一致);错误不自动回滚