Redis中Lua脚本的使用

参考资料:cnblogs.comRedis

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 线程。

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

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

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

避免复杂 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 逻辑。通过 EVALEVALSHASCRIPT LOAD 等命令执行和管理脚本,结合良好设计和测试策略,可显著提升 Redis 的开发效率与运行稳定性。