java操作lua脚本实例

前言

在上一篇文章 Redis中使用Lua脚本来实现并发下的原子操作 中我对Lua语言的一些简单的语法及其在Redis中的操作进行了介绍,但是在Java开发中我们还需要进一步的学习才能使这种技术落地。今天就结合Spring Data Redis这个我们经常使用的Redis开发组件来实际尝试一下Lua 脚本。

Lua 实现抽奖

模拟一个抽奖场景,从奖池中进行随机抽奖。规则如下:

中奖的人只能从奖池中抽取。

每个人只能中奖一次。

中奖总人数不能超过奖项的设置数。

生成中奖名单。

规则有了,我们先来分析如何使用Redis实现。Redis提供了SET集合,这种集合有点类似Java中的Set,放无重复的元素而且是无序的,可以满足随机性和奖池候选人的唯一性。同时它还提供了很多操作来满足抽奖的需要。接下来我们进行一一演示。

Redis SET 的一些操作。

基于篇幅我这里只演示一些抽奖可以用的上的Redis操作。

SET添加元素。

添加一个到多个元素,使用SADD命令往lottery中添加多个元素来模拟往奖池中加人。

127.0.0.1:6379> sadd lottery u1 u2 u3 u4 u5 u6 u7
(integer) 7
127.0.0.1:6379> sadd lottery u1
(integer) 0

如果没有lottey这个key就新建该key,有就直接添加并返回成功添加的元素个数。同时你会发现如果集合中存在了添加的元素是无法被再次添加的。

查询集合中的元素

查询所有元素通过SMEMBERS命令。

127.0.0.1:6379> smembers lottery
1) "u2"
2) "u7"
3) "u6"
4) "u4"
5) "u1"
6) "u3"
7) "u5"

随机抽取N个元素

SET集合有两个命令都能满足随机抽取N个元素,分别是SPOP和SRANDMEMBER,它们的区别在于SPOP会将选中的元素从原来的集合中剔除,而SRANDMEMBER不会。我们分别来使用这两个命令来随机从lottery中抽取2个元素来看看。

127.0.0.1:6379> srandmember lottery 2
1) "u2"
2) "u4"
127.0.0.1:6379> smembers lottery
1) "u2"
2) "u7"
3) "u6"
4) "u4"
5) "u1"
6) "u3"
7) "u5"
127.0.0.1:6379> spop lottery 2
1) "u3"
2) "u5"
127.0.0.1:6379> smembers lottery
1) "u2"
2) "u7"
3) "u6"
4) "u4"
5) "u1"

就lottery来说,如果你的奖池人数一次性添加的不再增加使用SPOP;如果动态添加,为了保证中奖的人不再次进入奖池应该使用SRANDMEMBER。

抽奖脚本

接下来就是抽奖脚本,我们从lottery中抽出特定的人放入中奖名单,另外一个集合chosen中。

按道理Redis抽奖脚本在Lua中应该是这样的:

function draw(KEYS,ARGV)

-- 抽奖逻辑 函数体

end

但是我们只需要编写抽奖逻辑的函数体,然后把函数体写入.lua文件中,在Maven项目中放入META-INF/scripts文件夹中,如图所示:

Redis结合Lua脚本实现抽奖逻辑

约定lua脚本所在的目录

draw.lua的逻辑为:
--- 简单抽奖脚本 return 结果最终传递给Java 应用
-- 奖池的key
local lottery_key = KEYS[1]
-- 中奖名单的key
local chosen_key = KEYS[2]
-- 预定抽奖的人数
local lottery_count = ARGV[1]
-- 如果预定抽奖的人数大于0才开始抽奖
if tonumber(lottery_count) > 0 then
-- 奖池中抽奖 返回的是 被抽中的人组成的数组
local chosen_list = redis.call('SRANDMEMBER', lottery_key, lottery_count);
-- 将抽中的人添加到中奖名单中 返回中奖的人数
if chosen_list then
return redis.call('SADD', chosen_key, unpack(chosen_list))
else
return 0
end
else
return 0
end
对应的 Java 代码
Spring Data Redis中的RedisTemplate提供了execute方法来执行Lua脚本,这里我选择使用下面的方法:@Override
public T execute(RedisScript script, List keys, Object... args) {
return scriptExecutor.execute(script, keys, args);
}

RedisScript Redis脚本的抽象,用来加载脚本。

keys对应Lua脚本中的KEYS,用来传入Redis的KEY,在Lua脚本中可以通过 KEYS[索引]来取值,例如取第一个值KEYS[1]。

args用来向Lua脚本传递其它的参数,在Lua脚本中可以通过ARGV[索引]来取值。

我们利用draw.lua脚本从Redis的lottery集合中抽取5名幸运者并把他们添加到中奖名单chosen集合中:

RedisScript redisScript = RedisScript.of(new ClassPathResource("META-INF/scripts/draw.lua"), Long.class);

Long chosenCount = stringRedisTemplate.execute(redisScript, Arrays.asList("lottery", "chosen"), Collections.singletonList("5"));

构造RedisScript对象时务必指定返回值对象以保证Lua脚本对象和Java的返回值能对应上,

否则将出现异常。参见org.springframework.data.redis.connection.ReturnType枚举。

总结

到此Redis利用Lua脚本进行抽奖的整套逻辑就完成了。Lua脚本在Redis中通常是为了保证高并发下的原子性,当你考虑是否需要使用它时应该充分考虑你的业务和架构是否适合使用它。