Redis整合lua脚本的实例分析
基于Redis的lua脚本能确保Redis命令的顺序性和原子性,所以在高并发场景下会用两者整合的方法实现限流和防超卖等效果,下面给出相关范例。
1、以计数模式实现限流效果
限流是指某应用模块需要限制指定IP(或指定模块、指定应用)在单位时间内的访问次数。例如,在某高并发场景里,会员查询模块对风险控制模块的限流需求是在10秒里最多允许有1000个请求。以计数模式的限流做法是,提供服务的模块会统计服务请求模块在单位时间内的访问次数,如果已经达到限流标准,就不予服务,反之则提供服务。
在如下的lua脚本里将实现基于计数模式的限流功能。
local obj=KEYS[1]
local limitNum=tonumber(redis.call('get',obj) or "0")
if curVisitNum+1>limitNum then
return 0
else
redis.call("INCRBY",obj,"1")
redis.call("EXPIRE",obj,tonumber(ARGV[2]))
return curVisitNum+1
end
该脚本共有3个参数:KEYS[1]用来接收待限流的对象,ARGV[1]表示限流的次数,ARGV[2]表示限流的时间单位。该脚本的功能是限制KEYS[1]对象在ARGC[2]时间范围内只能访问ARGV[1]次。
在第1行里,首先用KEYS[1]接收待限流的对象,比如模块或应用等,并把它赋给obj变量。在第2行里,把用ARGV[1]参数接收到的表示限流次数的对象赋给limitNum,注意这里需要用tonumber方法把包含限流次数的ARGV[1]参数转为数值类型。在第3行里,通过redis.call
方法调用get命令去获取待限流对象当前的访问次数,并赋给curVisitNum变量,如果获取不到,表示当前对象还没有访问,就把curVisitNum变量设置为0.
在第4行里,通过if语句判断待先流对象的访问次数是否达到限流标准。如果是就执行第5行的代码,通过return语句返回0.如果没有达到限流标准,就执行第7行到第9行的代码,首先通过INCRBY
命令对访问次数加1,然后通过EXPIRE
命令设置表示访问次数的键值对的生存时间,即限流的时间范围,最后通过return语句返回当前对象的访问次数。
也就是说,在调用该Lua脚本时,如果返回值是0,就说明当前访问量已经达到限流标准,否则还可以继续访问。在如下的Java代码中,将调用上述脚本,实现限流效果。
import redis.clients.jedis.Jedis;
public class LuaLimitByCount extends Thread {
@Override
public void run() {
Jedis jedis=new Jedis("192.168.159.33",6379);
//在本线程内,模拟在单位时间内发5个请求
for(int visitNum=0;visitNum<5;visitNum++){
boolean visitFlag=LimitByCount.canVisit(jedis,Thread.currentThread().getName(),"10","3");
if(visitFlag){
System.out.println(Thread.currentThread().getName()+" can visit.");
}else{
System.out.println(Thread.currentThread().getName()+" can not visit.");
}
}
}
public static void main(String[] args) {
//开启3个线程
for(int cnt=0;cnt<3;cnt++){
new LuaLimitByCount().start();
}
}
}
//封装是否需要限流的方法
class LimitByCount{
//判断是否需要限流
public static boolean canVisit(Jedis jedis,String modelName,String limitTime,
String limitNum){
String script="local obj=KEYS[1] \n" +
"local limitNum=tonumber(ARGV[1]) \n" +
"local curVisitNum=tonumber(redis.call('get',obj) or \"0\")\n" +
"if curVisitNum+1>limitNum then \n" +
" return 0\n" +
"else \n" +
" redis.call(\"incrby\",obj,\"1\")\n" +
" redis.call(\"expire\",obj,\"10\")\n" +
" return curVisitNum+1\n" +
"end";
String retVal=jedis.eval(script,1,modelName,limitNum,limitTime).toString();
if("0".equals(retVal)){
return false; //不能继续访问
}else{
return true;
}
}
}
在main函数里,通过for循环启动了3个线程,并通过它们的run方法在短时间里调用5次LimitByCount类的canVisit方法。结果如下:
可以看到,在main函数里创建的3个线程均只有3次请求得到允许,其他请求超过了限流最大访问量,所以被“限流”了。
2、用lua脚本防止超卖
超卖是指在秒杀活动里多卖出了商品,比如某秒杀系统里最多只能卖出100件,但是并发控制没有做好,最终有100多个请求下单成功,这样就会给商家造成损失。
lua脚本天然具有原子性,而且执行lua脚本的Redis服务器是以单线程模式处理命令,所以用lua脚本能有效地防止超卖。在如下的lua脚本里实现了防超卖的效果。该lua脚本只有一个KEYS[1]参数,用来传入表示商品的键。
local existedNum=tonumber(redis.call('get',KEYS[1]))
if(existedNum>0) then
redis.call('incrby',KEYS[1],-1)
return existedNum
end
return -1
在运行该脚本前,需要确保Redis服务器已经存在(KEYS[1],商品个数)这个键值对。在第1行里,先通过
redis.call
方法调用get命令,获得该商品当前的存货数,如果通过第2行的if判断发现大于0,就先通过第3行的incrby
命令对该商品的存货书进行减1操作,并通过第4行的语句返回当前的商品存货数,反之则执行第6行的语句,返回-1.也就是说,如果运行该脚本得到-1,就说明该次请求会导致超卖,否则能继续后继的购买动作。
用Java代码调用lua脚本演示防止超卖的效果。
package com.baizhi;
import redis.clients.jedis.Jedis;
public class AvoidSellTooMuch extends Thread {
@Override
public void run() {
Jedis jedis=new Jedis("192.168.159.33",6379);
//在本线程内,模拟在单位时间内发5个请求
boolean sellFlag=CheckUtil.canSell(jedis,"Computer");
if(sellFlag){
System.out.println(Thread.currentThread().getName()+" can buy.");
}else{
System.out.println(Thread.currentThread().getName()+" can not buy.");
}
}
public static void main(String[] args) {
//创建同Redis的连接
Jedis jedis=new Jedis("192.168.159.33",6379);
//预设5个电脑商品,并设置10秒的生存时间
jedis.set("Computer","5");
jedis.expire("Computer",10);
//开启10个线程来抢购
for(int cnt=0;cnt<10;cnt++){
new AvoidSellTooMuch().start();
}
}
}
class CheckUtil{
//判断当前请求是否会导致超卖
public static boolean canSell(Jedis jedis,String modelName){
//防止超卖的脚本
String script="local existedNum=tonumber(redis.call('get',KEYS[1]))\n" +
"if(existedNum>0) then\n" +
" redis.call('incrby',KEYS[1],-1)\n" +
" return existedNum\n" +
"end\n" +
"return -1\n";
String retVal=jedis.eval(script,1,modelName,modelName).toString();
if("-1".equals(retVal)){
return false; //不能继续访问
}else{
return true;
}
}
}
在main函数里,首先通过set命令在Redis数据库里设置了5件“Computer”商品,并通过expire命令设置了该键值对的生存时间。随后用for循环启动了10个线程,让这10个线程去抢购5个"Computer"商品。每次运行这个代码的输出结果未必相同,下面给出其中某一次的运行结果。
这10个线程里有5个线程成功地抢购到商品,另外5个线程没有抢购到,并没有出现超卖现象。多运行几次代码就会发现,每次抢购到商品地线程号未必相同,但是每次只有5个线程能抢购到,不会出现超卖现象。
- 点赞
- 收藏
- 关注作者
评论(0)