Redis中Lua脚本的使用

参考资料:cnblogs.com、Redis

Lua 脚本介绍

Redis 从 2.6 版本开始内置 Lua 脚本执行引擎,用于在服务器端组合多个 Redis 命令为一个原子操作,从而避免客户端多次往返网络、保证复杂逻辑的原子性执行。Lua 是一种轻量级、可嵌入的脚本语言,非常适合嵌入 Redis 这种高性能内存数据库中。

Lua 在 Redis 中执行时无需额外配置,只需通过命令将脚本发送到 Redis。脚本由 Redis Server 内的 Lua 5.1 解释器运行。Redis 脚本将命令组合在一起,避免多次网络往返,并在执行期间阻塞其他客户端命令,确保原子性。脚本执行结果可返回值给客户端。

Lua 脚本基本语法

Lua 语法简单,适合嵌入式脚本应用。基本数据类型包括:nil(空)、boolean(布尔值)、number(数字)、string(字符串)和 table(表)。table 是 Lua 的核心数据结构,它既可表现为数组也可表现为字典。

变量声明:

1
2
name = 'example'       -- 全局变量
local age = 18 -- 局部变量

表(类似数组、字典):

1
2
local arr = {'a', 'b', 1}        -- 数组样
local dict = {key='value'} -- 字典样

控制结构类似其他语言:

1
2
3
4
5
6
7
if x < 10 then
print('small')
elseif x < 20 then
print('medium')
else
print('large')
end

循环遍历表:

1
2
3
for i,v in ipairs(arr) do
print(i, v)
end

返回值可以使用 return,Lua 支持返回多个值,但在 Redis 脚本中推荐返回一个表以避免客户端解析混乱。

Lua 脚本在 Redis 中的使用方式

在 Redis 中执行 Lua 脚本核心命令是 EVAL:

1
EVAL <script> <numkeys> <key1> ... <keyN> <arg1> ... <argM>
  • <script>:Lua 脚本内容
  • <numkeys>:脚本使用的 Redis 键数量
  • 接下来是 <key1> ... keyN,脚本中通过 KEYS[1] 等访问
  • 余下是参数通过 ARGV[1] 等访问。

键与参数分离的目的是让 Redis 能够基于 numkeys 提前分析访问的键,有助于集群模式下正确路由。

1
127.0.0.1:6379> EVAL "return redis.call('GET', KEYS[1])" 1 mykey

Redis 还提供了脚本缓存机制,通过 SCRIPT LOAD 加载脚本并返回 SHA1 摘要,然后可用 EVALSHA 执行,避免每次发送完整脚本字符串,提高性能。

1
2
SCRIPT LOAD "return 'hello'"
EVALSHA <sha1> 0

Redis 中 Lua 脚本的特性与原理

Lua 脚本在 Redis 内部执行时具有以下关键特性:

  • 原子性:整个脚本在执行期间不会被打断,所有命令作为一个原子事务运行。
  • 阻塞执行:执行期间 Redis 不处理其他客户端命令,因此脚本应保持短小以避免阻塞客户端。
  • 数据类型转换:Lua 与 Redis 之间数据互传需进行类型转换,例如 Lua 返回的小数可能被转换成整数。
  • 错误处理:redis.call() 在命令错误时会抛出错误,中断脚本;redis.pcall() 则返回错误表,可在脚本内部捕获处理。

Redis 中的最佳实践

脚本设计
Lua 脚本应尽量保持短、逻辑简单。避免大型循环或长耗时计算,因为它们会阻塞 Redis 线程。

参数与键分离
使用 KEYS 和 ARGV 明确区分 Redis 键和参数。键用于 Redis 访问,参数用于业务逻辑。Redis 集群模式下所有键应在同一槽位以避免错误脚本行为。

缓存机制
尽可能通过 SCRIPT LOAD 和 EVALSHA 重用脚本,减少网络传输和重复编译开销。

错误与测试
在上线前全面测试脚本逻辑。脚本执行失败不会自动回滚之前的修改。需要在业务层处理失败后的状态一致性。

避免复杂 Lua 特性
Lua 有许多语言特性如协程、复杂函数定义等,但在 Redis 脚本中应避免使用,聚焦于简单逻辑以提高可维护性。

以下内容在 Redis + Lua 的工程语境下展开,强调为什么用、怎么用、避免什么。

Redis 使用实例

库存扣减(高并发原子性)

场景

秒杀、抢购、优惠券库存扣减。
要求:不能超卖、不能多扣、必须原子执行。

问题本质

以下逻辑如果拆散为多条命令,会产生竞态条件:

  1. 查询库存
  2. 判断是否大于 0
  3. 扣减库存

Lua 脚本实现

1
2
3
4
5
6
local stock = tonumber(redis.call('GET', KEYS[1]))
if not stock or stock <= 0 then
return -1
end
redis.call('DECR', KEYS[1])
return stock - 1

Redis 调用

1
EVAL "<lua-script>" 1 stock:sku:1001

价值

  • 单线程原子执行
  • 无需 Redis 事务(MULTI/EXEC)
  • 避免客户端 CAS 自旋
  • 秒杀场景标准解法

分布式限流(滑动窗口)

场景

接口防刷、短信验证码、登录频控。

限流规则示例

  • 每个用户
  • 10 秒内最多 5 次

Lua 脚本实现(ZSET)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)

if count >= limit then
return 0
end

redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1

特点

  • 精确滑动窗口
  • 清理 + 判断 + 写入一次完成
  • 避免多命令时间漂移

价值

  • 高并发下限流结果一致
  • Lua 是唯一正确解法

防重复提交 / 幂等控制

场景

  • 表单重复提交
  • 订单重复创建
  • MQ 消费幂等

典型逻辑

  • key 不存在 → 执行业务
  • key 存在 → 拒绝

Lua 脚本

1
2
3
4
5
if redis.call('EXISTS', KEYS[1]) == 1 then
return 0
end
redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])
return 1

优点

  • SETNX + EXPIRE 原子化
  • 不依赖 Redis 版本是否支持 SET key value NX EX

Java 中调用 Redis Lua 脚本

StringRedisTemplate 调用方式

适用场景

  • key / value 都是字符串
  • 绝大多数业务场景首选

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
String lua =
"local stock = tonumber(redis.call('GET', KEYS[1])) " +
"if not stock or stock <= 0 then return -1 end " +
"redis.call('DECR', KEYS[1]) return stock - 1";

DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(lua);
script.setResultType(Long.class);

Long result = stringRedisTemplate.execute(
script,
Collections.singletonList("stock:sku:1001")
);

关键点

  • KEYS → List 传入
  • ARGV → execute 后的可变参数
  • 返回值类型必须显式声明

RedisTemplate 调用方式

适用场景

  • value 是对象、JSON、复杂结构
  • 使用序列化器

示例

1
2
3
4
5
6
7
8
9
10
11
12
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(lua);
script.setResultType(Long.class);

List<String> keys = Arrays.asList("stock:sku:1001");

Long result = redisTemplate.execute(
script,
redisTemplate.getStringSerializer(),
redisTemplate.getValueSerializer(),
keys
);

总结

Redis 内置 Lua 脚本功能用于将一系列命令组合为一个原子操作,提高性能、减少网络往返、保证逻辑一致性。Lua 语法简单,适合轻量级编写 Redis 逻辑。通过 EVAL、EVALSHA、SCRIPT LOAD 等命令执行和管理脚本,结合良好设计和测试策略,可显著提升 Redis 的开发效率与运行稳定性。