Redis 中的 Lua 脚本到底是什么?小白也能看懂
Redis 中的 Lua 脚本到底是什么?小白也能看懂
最近在学习 Redis、分布式锁、优惠券秒杀这些内容时,经常会看到 Lua 脚本。
比如 Redisson 分布式锁底层会用 Lua,优惠券扣库存也经常会用 Lua。刚开始看到的时候会觉得很懵:Lua 是 Redis 特有的吗?为什么 Redis 里面还能写脚本?它的语法又是什么?
这篇文章就用小白能理解的方式,把 Redis Lua 脚本讲清楚。
一、Lua 脚本是 Redis 特有的吗?
不是。
Lua 本身是一门独立的脚本语言,和 Java、Python、JavaScript 一样,只是它比较轻量,常常被嵌入到其他软件里面使用。
比如:
Redis 可以执行 Lua
Nginx 可以使用 Lua
游戏开发里经常使用 Lua
一些嵌入式系统也会使用 Lua
所以准确来说:
Lua 不是 Redis 特有的,Redis 只是支持执行 Lua 脚本。
在 Java 后端开发里,我们最常见到 Lua 的场景,通常就是 Redis。
二、Redis 为什么要用 Lua?
Redis 本身每一条命令都是原子的。
比如:
INCR stock
这条命令执行时不会被其他命令打断。
但是如果一个业务逻辑需要多条 Redis 命令组合起来,就可能会出现并发问题。
比如优惠券秒杀场景:
1. 判断库存是否大于 0
2. 判断用户是否已经领取过
3. 库存减 1
4. 记录用户已经领取
如果这些操作放在 Java 代码里分多次执行:
String stock = redis.get("stock");
if (stock > 0) {
redis.decr("stock");
redis.sadd("coupon:users", userId);
}
中间可能会被其他线程插队。
比如库存只剩 1 个时,两个请求同时进来:
线程 A 看到 stock = 1
线程 B 也看到 stock = 1
线程 A 扣库存成功
线程 B 也扣库存成功
最后库存可能变成 -1
这就是典型的并发问题。
而 Lua 脚本可以把多条 Redis 命令打包成一个整体,让 Redis 一次性执行。
核心点是:
Redis 执行 Lua 脚本时,脚本整体是原子的。
也就是说,Lua 脚本执行过程中,不会被其他 Redis 命令插队。
所以 Redis 使用 Lua 的核心目的就是:
把多个 Redis 命令组合成一个原子操作。
三、Redis Lua 脚本长什么样?
先看一个 Redisson 分布式锁里类似的 Lua 逻辑:
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
end
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
redis.call('hincrby', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return nil
end
return redis.call('pttl', KEYS[1])
刚开始不用完全看懂,先抓住几个关键点:
if ... then ... end 条件判断
redis.call(...) 调用 Redis 命令
KEYS[1] 外部传进来的第 1 个 key
ARGV[1] 外部传进来的第 1 个普通参数
return 返回结果
Redis Lua 脚本里面最常用的东西,其实就这些。
四、redis.call 是什么?
redis.call() 是 Redis 提供给 Lua 脚本的函数。
可以理解为:
redis.call('Redis命令', '参数1', '参数2', ...)
它的作用就是:在 Lua 脚本里面调用 Redis 命令。
比如在 Redis 命令行里执行:
GET name
在 Lua 里就是:
redis.call('get', 'name')
Redis 命令行:
SET name frank
Lua 里:
redis.call('set', 'name', 'frank')
Redis 命令行:
HSET user:1 name frank
Lua 里:
redis.call('hset', 'user:1', 'name', 'frank')
所以可以简单记:
redis.call() = 在 Lua 脚本里执行 Redis 命令
五、KEYS 和 ARGV 是什么?
Redis 执行 Lua 脚本时,可以从外面传参数进去。
这些参数分成两类:
KEYS:专门用来传 Redis 的 key
ARGV:用来传普通参数
比如执行:
EVAL "return redis.call('get', KEYS[1])" 1 name
这里的含义是:
脚本内容:return redis.call('get', KEYS[1])
key 的数量:1
KEYS[1] = name
所以脚本里的:
redis.call('get', KEYS[1])
就等价于:
GET name
如果有两个 key 和一个普通参数:
EVAL "return {KEYS[1], KEYS[2], ARGV[1]}" 2 key1 key2 value1
对应关系就是:
KEYS[1] = key1
KEYS[2] = key2
ARGV[1] = value1
注意,Lua 的数组下标通常从 1 开始。
所以不是:
KEYS[0]
ARGV[0]
而是:
KEYS[1]
ARGV[1]
这一点和 Java 不一样。
六、Lua 基础语法
在 Redis 使用 Lua 时,不需要系统学习完整 Lua 语言。先掌握几个常用语法就够了。
1. 定义变量
Lua 定义变量使用 local:
local name = "frank"
local age = 24
类似 Java 中的局部变量:
String name = "frank";
int age = 24;
Lua 不需要显式声明类型。
2. 字符串
Lua 字符串可以用单引号,也可以用双引号:
local name = "frank"
local city = 'nanjing'
3. 数字
local count = 10
local price = 99.9
4. 条件判断
Lua 的 if 写法是:
if 条件 then
-- 执行逻辑
end
比如:
local stock = 10
if stock > 0 then
return 1
end
带 else 的写法:
local stock = 0
if stock > 0 then
return 1
else
return 0
end
对应 Java:
if (stock > 0) {
return 1;
} else {
return 0;
}
5. 相等和不等判断
Lua 判断相等使用:
==
比如:
if stock == 0 then
return 0
end
Lua 判断不等使用:
~=
比如:
if stock ~= 0 then
return 1
end
注意,不是 Java 里的:
!=
6. 注释
单行注释:
-- 这是单行注释
多行注释:
--[[
这是多行注释
]]
七、Redis Lua 中要注意类型转换
Redis 中很多数据取出来默认是字符串。
比如 Redis 中有:
SET stock 10
Lua 中这样取:
local stock = redis.call('get', KEYS[1])
此时 stock 很可能是字符串 "10"。
如果要做数字比较,最好使用 tonumber() 转成数字:
local stock = tonumber(redis.call('get', KEYS[1]))
if stock > 0 then
return 1
else
return 0
end
tonumber() 类似 Java 里的:
Integer.parseInt(stock)
所以 Redis Lua 里经常会看到:
tonumber(redis.call('get', KEYS[1]))
意思就是:
从 Redis 里取出值,然后转成数字。
八、最简单的 Lua 脚本例子
假设 Redis 中有:
SET stock 10
Lua 脚本:
local stock = redis.call('get', KEYS[1])
return stock
如果传入:
KEYS[1] = stock
那么这个脚本做的事情就是:
获取 stock 的值并返回。
逐行解释:
local stock = redis.call('get', KEYS[1])
表示从 Redis 中获取 KEYS[1] 对应的值,并保存到变量 stock 中。
return stock
表示返回这个值。
九、实战例子:判断库存是否充足
假设 Redis 中有:
SET coupon:stock 10
Lua 脚本:
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
return 0
end
return 1
含义是:
1. 从 Redis 获取库存
2. 转成数字
3. 如果库存小于等于 0,返回 0
4. 否则返回 1
返回值可以约定为:
0:库存不足
1:库存充足
十、实战例子:扣减库存
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
return 0
end
redis.call('decr', KEYS[1])
return 1
这段脚本做了三件事:
1. 获取库存
2. 判断库存是否大于 0
3. 如果库存充足,就执行库存减 1
因为这段 Lua 脚本是在 Redis 中一次性执行的,所以它整体是原子的。
不会出现两个请求同时看到库存为 1,然后都扣减成功的问题。
十一、实战例子:防止用户重复领取优惠券
优惠券秒杀场景中,通常不只是扣库存,还要防止同一个用户重复领取。
假设有两个 key:
KEYS[1] = 优惠券库存 key
KEYS[2] = 用户领取记录 key
比如:
KEYS[1] = coupon:1001:stock
KEYS[2] = coupon:1001:users
其中:
coupon:1001:stock 表示优惠券库存
coupon:1001:users 表示已经领取过该优惠券的用户集合
然后传入一个普通参数:
ARGV[1] = userId
完整 Lua 脚本如下:
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
return 0
end
local isMember = redis.call('sismember', KEYS[2], ARGV[1])
if isMember == 1 then
return 2
end
redis.call('decr', KEYS[1])
redis.call('sadd', KEYS[2], ARGV[1])
return 1
返回值可以约定为:
0:库存不足
1:领取成功
2:用户已经领取过
十二、逐行解释优惠券 Lua 脚本
local stock = tonumber(redis.call('get', KEYS[1]))
从 Redis 中获取优惠券库存,并转成数字。
if stock <= 0 then
return 0
end
如果库存小于等于 0,说明库存不足,直接返回 0。
local isMember = redis.call('sismember', KEYS[2], ARGV[1])
判断当前用户是否已经领取过该优惠券。
它等价于 Redis 命令:
SISMEMBER coupon:1001:users userId
if isMember == 1 then
return 2
end
如果用户已经领取过,就返回 2。
redis.call('decr', KEYS[1])
优惠券库存减 1。
redis.call('sadd', KEYS[2], ARGV[1])
把当前用户 ID 添加到领取记录集合中。
return 1
返回领取成功。
十三、Redis 里怎么执行 Lua 脚本?
可以使用 EVAL 命令。
基本格式:
EVAL "Lua脚本" key数量 key1 key2 arg1 arg2
比如:
EVAL "return redis.call('get', KEYS[1])" 1 name
表示:
执行脚本:return redis.call('get', KEYS[1])
key 数量:1
KEYS[1] = name
如果有两个 key,一个普通参数:
EVAL "return {KEYS[1], KEYS[2], ARGV[1]}" 2 key1 key2 value1
对应关系是:
KEYS[1] = key1
KEYS[2] = key2
ARGV[1] = value1
十四、Java 中如何执行 Redis Lua?
在 Spring Boot 中,一般使用 StringRedisTemplate 执行 Lua 脚本。
示例:
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText("""
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
return 0
end
redis.call('decr', KEYS[1])
return 1
""");
redisScript.setResultType(Long.class);
Long result = stringRedisTemplate.execute(
redisScript,
List.of("coupon:1001:stock")
);
这里:
List.of("coupon:1001:stock")
对应 Lua 中的:
KEYS[1]
如果还要传用户 ID:
Long result = stringRedisTemplate.execute(
redisScript,
List.of("coupon:1001:stock", "coupon:1001:users"),
userId
);
那么对应关系就是:
KEYS[1] = coupon:1001:stock
KEYS[2] = coupon:1001:users
ARGV[1] = userId
十五、Redis Lua 最常用语法总结
初学 Redis Lua,不需要一开始就学完整 Lua 语言。
先记住下面这些就够了:
-- 定义变量
local value = "hello"
-- 调用 Redis 命令
local stock = redis.call('get', KEYS[1])
-- 转成数字
local num = tonumber(stock)
-- 条件判断
if num > 0 then
return 1
else
return 0
end
常用关键点:
local:定义局部变量
if then end:条件判断
return:返回结果
tonumber:转成数字
redis.call:调用 Redis 命令
KEYS[n]:外部传进来的 Redis key
ARGV[n]:外部传进来的普通参数
十六、Redis Lua 常见使用场景
Redis Lua 在后端开发中非常常见,尤其是高并发场景。
常见用途包括:
1. 分布式锁
2. 秒杀扣库存
3. 优惠券领取
4. 防重复提交
5. 接口限流
6. 原子判断 + 原子修改
比如 Redisson 的分布式锁底层就大量使用 Lua 脚本。
优惠券系统中,判断库存、扣减库存、记录用户领取信息,也很适合使用 Lua。
十七、面试怎么回答 Redis Lua?
可以这样回答:
Lua 不是 Redis 特有的,它是一门轻量级脚本语言。Redis 支持执行 Lua 脚本,主要是为了把多个 Redis 命令组合成一个原子操作。比如优惠券秒杀场景中,需要判断库存、判断用户是否重复领取、扣减库存、记录用户领取信息。如果这些操作在 Java 里分多次执行,可能会出现并发问题;放到 Lua 脚本里交给 Redis 执行,可以保证脚本整体的原子性。
再补充一句:
Redis Lua 常用于分布式锁、秒杀扣库存、接口限流、防重复提交等场景。
这样回答就比较完整。
十八、总结
Lua 不是 Redis 特有的,它是一门独立的轻量级脚本语言。
Redis 支持 Lua 的主要原因,是为了让开发者可以把多条 Redis 命令组合成一个原子操作。
对于 Java 后端开发来说,学习 Redis Lua 不需要一开始就系统掌握完整 Lua 语言。先掌握下面几个点就够了:
1. redis.call() 用来调用 Redis 命令
2. KEYS[n] 表示外部传入的 Redis key
3. ARGV[n] 表示外部传入的普通参数
4. local 用来定义变量
5. if then end 用来做条件判断
6. tonumber() 用来把字符串转成数字
7. Lua 脚本在 Redis 中整体原子执行
可以把 Redis Lua 理解成:
把多条 Redis 命令打包成一个小程序,然后一次性交给 Redis 执行。
这就是 Redis Lua 最核心的价值。
更多推荐




所有评论(0)