Spring Redis中使用Lua脚本实现高并发原子操作
The following article is from 码农小胖哥 Author 码农小胖哥
1. 前言
在上一文中我对 Lua 语言的一些简单的语法及其在 Redis 中的操作进行了介绍,但是在 Java 开发中我们还需要进一步的学习才能使这种技术落地。今天就结合Spring Data Redis这个我们经常使用的 Redis 开发组件来实际尝试一下 Lua 脚本。
2. 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
文件夹中,如图所示:
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
这里的逻辑仅仅为了演示用,实际上要根据你的业务进行编写,lua 相关的语法请参考上一文。
3. 对应的 Java 代码
Spring Data Redis
中的RedisTemplate
提供了execute
方法来执行 Lua 脚本,这里我选择使用下面的方法:
@Override
public <T> T execute(RedisScript<T> script, List<K> 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<Long> 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
枚举。
4. 总结
到此 Redis 利用 Lua 脚本进行抽奖的整套逻辑就完成了。Lua 脚本在 Redis 中通常是为了保证高并发下的原子性,当你考虑是否需要使用它时应该充分考虑你的业务和架构是否适合使用它,而非为了“炫技”。
好了今天的分享就到这里,我是:码农小胖哥 多多关注
更多干货分享关注下方公众号
往期推荐
﹀
﹀
﹀
深度内容
推荐加入
最近热门内容回顾 #技术人系列