这道题的核心是:Redis 的 Lua 脚本可以把多个 Redis 操作组合成一个原子操作,避免操作执行到一半时被其他命令插入。
你之前学过的分布式锁释放操作,就是 Lua 脚本的典型应用。
一、为什么 Redis 需要 Lua 脚本?
先看一个分布式锁的例子。
假设请求 A 获得了锁,锁的值是 token-A。
释放锁时,需要做两件事:
- 判断锁是不是自己的。
- 如果是自己的,删除锁。
如果使用普通命令分开执行:
GET lock:order:1001DEL lock:order:1001可能出现下面的问题:
- 请求 A 执行
GET,发现锁属于自己。 - 此时锁过期了,请求 B 获得了这把锁。
- 请求 A 接着执行
DEL,把请求 B 的锁删除了。
问题就在于:判断和删除是两个独立操作,中间可能发生其他事情。
使用 Lua 脚本,就可以把这两个步骤合并成一个原子操作。
二、Lua 脚本怎么使用?
Redis 支持通过 EVAL 命令执行 Lua 脚本。
例如,安全地释放分布式锁:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1])else return 0end执行命令:
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:order:1001 token-A看起来有点复杂,我们拆开理解。
| 部分 | 含义 |
|---|---|
EVAL | 执行 Lua 脚本 |
1 | 后面有 1 个 Key 参数 |
lock:order:1001 | KEYS[1],锁的名称 |
token-A | ARGV[1],锁的唯一标识 |
redis.call() | 在 Lua 中调用 Redis 命令 |
return | 返回执行结果 |
脚本的执行逻辑是:
- 如果锁的值等于
token-A,就删除锁并返回1。 - 如果锁不存在或不属于
token-A,就不删除并返回0。
为什么它是原子的?
Redis 执行 Lua 脚本期间,不会穿插执行其他客户端的普通命令。
因此,从判断锁的归属到删除锁,中间不会插入其他客户端的命令。
这就解决了刚才的误删问题。
注意:Lua 脚本执行期间会阻塞 Redis 的其他命令,所以脚本应当尽量简短,避免执行耗时很长的循环。
三、除了分布式锁,还能用在哪里?
1. 扣减库存
例如,商品库存是 10,要扣减 1 件。
可以在一个 Lua 脚本中完成:
- 读取库存。
- 判断库存是否充足。
- 库存充足则扣减。
- 返回执行结果。
这样多个请求并发扣减时,不会因为读取和更新之间被其他命令插入而导致超卖。
2. 限流
例如,限制一个用户每分钟最多请求 100 次。
Lua 脚本可以把计数器更新、过期时间设置和阈值判断组合起来,避免多个步骤之间出现并发问题。
3. 多个操作需要保持整体原子性
当一项业务需要连续执行多个 Redis 命令,而且中间不能被其他客户端的命令打断时,可以考虑 Lua 脚本。
四、面试时怎么回答?
Redis 的 Lua 脚本功能允许我们在 Redis 中执行自定义脚本,将多个 Redis 操作组合成一个原子操作。
使用时,可以通过 EVAL 命令执行 Lua 脚本,并通过 KEYS 和 ARGV 传入 Key 和其他参数。
典型应用是分布式锁的释放:通过 Lua 脚本先判断锁的唯一标识是否属于当前请求,再决定是否删除锁,保证判断和删除的原子性,避免误删其他请求的锁。
此外,Lua 脚本还可以用于库存扣减、限流等需要多个 Redis 操作保持原子性的场景。
需要注意,Lua 脚本执行期间会阻塞 Redis 的其他命令,因此脚本应该尽量简短。
五、记住三个重点
- 作用: 将多个 Redis 操作组合成一个原子操作。
- 使用:
EVAL执行脚本,通过KEYS和ARGV传参。 - 典型场景: 分布式锁释放、库存扣减、限流。
面试官如果继续追问,最常见的就是:Redis Lua 脚本为什么具有原子性? 答案是脚本执行期间不会穿插执行其他客户端命令,但这不代表脚本发生运行时错误后,之前执行过的写操作会自动回滚。