Redis整合lua脚本的实例分析

举报
别团等shy哥发育 发表于 2023/02/04 11:36:26 2023/02/04
【摘要】   基于Redis的lua脚本能确保Redis命令的顺序性和原子性,所以在高并发场景下会用两者整合的方法实现限流和防超卖等效果,下面给出相关范例。 1、以计数模式实现限流效果  限流是指某应用模块需要限制指定IP(或指定模块、指定应用)在单位时间内的访问次数。例如,在某高并发场景里,会员查询模块对风险控制模块的限流需求是在10秒里最多允许有1000个请求。以计数模式的限流做法是,提供服务的模...

  基于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个线程能抢购到,不会出现超卖现象。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。