前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis整合lua脚本的实例分析

Redis整合lua脚本的实例分析

作者头像
别团等shy哥发育
发布2023-02-25 15:03:58
5940
发布2023-02-25 15:03:58
举报
文章被收录于专栏:全栈开发那些事

文章目录

  基于Redis的lua脚本能确保Redis命令的顺序性和原子性,所以在高并发场景下会用两者整合的方法实现限流和防超卖等效果,下面给出相关范例。

1、以计数模式实现限流效果

  限流是指某应用模块需要限制指定IP(或指定模块、指定应用)在单位时间内的访问次数。例如,在某高并发场景里,会员查询模块对风险控制模块的限流需求是在10秒里最多允许有1000个请求。以计数模式的限流做法是,提供服务的模块会统计服务请求模块在单位时间内的访问次数,如果已经达到限流标准,就不予服务,反之则提供服务。 在如下的lua脚本里将实现基于计数模式的限流功能。

代码语言:javascript
复制
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代码中,将调用上述脚本,实现限流效果。

代码语言:javascript
复制
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]参数,用来传入表示商品的键。

代码语言:javascript
复制
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脚本演示防止超卖的效果。

代码语言:javascript
复制
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个线程能抢购到,不会出现超卖现象。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-04-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 1、以计数模式实现限流效果
  • 2、用lua脚本防止超卖
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档