当前位置: 首页 > article >正文

lua脚本实现滑动窗口的分布式全局限流器, 控制api接口qps

前言

限流器, 从算法实现的角度来说, 就我知道的常见的有 滴漏桶, 令牌桶, 滑动窗口计数,固定窗口计数法

从实现的工具来说, 常见的有 guava的 RateLimiter (令牌桶)
redis的每秒或者每一分钟过期时间的incr(固定窗口计数)

但是这些大多数时候都被我们用来当做单个机器上的限流措施, 尤其是guava这种单体框架. redis的incr虽然能控制全局, 但是还是有问题.

场景

对接支付系统的接口, 给出的qps是5, 一开始是直接在代码中sleep200毫秒, 后面发现分布式的集群这种限制是行不通的.
使用incr方法? 不行, 因为你的窗口期1s不一定就是下游系统的窗口期. 比如下面这样, 虽然在我们这边保证了1秒内请求5次, 但是也许在别人系统的窗口中正好是你两个窗口期集中请求的部分, 就会报错
[00 000][0000 0]
[…] [111111]

lua脚本

这里采用滑动窗口计数的方式, 这样能保证不管什么时间段, 这个前后1s内总是不会超过5个请求, 也就能满足下游的要求

-- 获取zset的key
local key = KEYS[1]
-- 脚本传入的限流大小
local limit = tonumber(ARGV[1])
-- 区间长度
local rangeMill = tonumber(ARGV[2])
-- 当前网络时间戳 秒和这秒过去的微秒数
local date =  redis.call("time")
-- 得到微妙时间戳 微秒
local now = tonumber(date[1])*1000000 + tonumber(date[2])
-- 根据区间获取起始时间戳 微秒
local start = now - rangeMill*1000
-- 过期时间s
local rangeSec = tonumber(math.ceil(rangeMill / 1000))
if rangeSec == 0 then
    rangeSec = 1
end
-- 脚本传入的uuid
local uuid = ARGV[3]
-- 获取当前流量总数
local count = tonumber(redis.call('zcount',key, start, now))
--是否超出限流值
if count + 1 >limit then
    return false
-- 不需要限流
else
    -- 添加当前访问时间戳到zset
    redis.call('zadd', key, now, uuid)
    -- 移除时间区间以外不用的数据,不然会导致zset过大
    redis.call('zremrangebyscore',key, 0, start)
    redis.call("expire", key,rangeSec)
    return true
end

然后使用redisTemplate去调用这个脚本

    /**
     * 读取lua脚本
     * @return
     */
    @Bean("limitRateScript")
    public RedisScript<Boolean> loadRedisScript(){
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        //lua脚本路径
        redisScript.setLocation(new ClassPathResource("lua/token_rate_limit.lua"));
//        redisScript.setScriptText(lua);
        //lua脚本返回值
        redisScript.setResultType(java.lang.Boolean.class);
        return redisScript;
    }
/**
     * 通过lua脚本实现令牌桶
     *
     * @param key        key
     * @param limit      限制次数
     * @param rangeMills 区间,ms
     * @return
     */
    public static boolean limit(String key, int limit, int rangeMills) {
        RedisScript<Boolean> redisScript = SpringCtxUtil.getBean("limitRateScript", RedisScript.class);
        //当前时间戳
        return Boolean.TRUE.equals(getStringRedisTemplate().execute(
                //lua限流脚本
                redisScript,
                //限流资源名称
                Collections.singletonList(key),
                //限流大小
                String.valueOf(limit),
                //限流窗口的区间大小
                String.valueOf(rangeMills),
                //id值,保证zset集合里面不重复,不然会覆盖
                UUID.randomUUID().toString()

        ));
    }

    /**
     * 使用令牌桶限流,但是有一个等待时间
     *
     * @param key         key
     * @param limit       限制次数
     * @param rangeMill   区间,ms
     * @param waitTimeOut
     * @param timeUnit
     */
    public static void limitWait(String key, int limit, int rangeMill, int waitTimeOut, TimeUnit timeUnit) {
        long startTm = System.currentTimeMillis();
        while (System.currentTimeMillis() - startTm <= timeUnit.toMillis((long) waitTimeOut)) {
            boolean b = limit(key, limit, rangeMill);
            if (b) {
                return;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(100L);
            } catch (InterruptedException var9) {
                var9.printStackTrace();
            }
        }
        throw new Exception("wait limit lock timeout|" + key);
    }

实际效果

使用jmeter模拟100线程循环5次
大概27s执行完, 其中失败率77%, 即成功的是 500 * 23%=115
符合qps=5的预期 (比27*5少, 其中少的部分是因为基本不可能做好所有请求都等上一个窗口完了以后完美衔接)

如果把窗口边界时间作为参数给lua, 会出现超频的问题
最开始, 窗口的起始时间, 结束时间, 都是由业务传给lua的, 但是通过jemter发现还是触发了qps超频的错误,尤其是启动后第一次批量请求

原因大概是因为一开始大量的请求挤进来, 每个线程都生成相近的边界时间, 比如A是 09:00:00 000 ~ 09:00:01 000,
B是 09:00:00 100 ~ 09:00:01 100, 但是B却优先从jedis连接池拿到了redis连接, 然后在09:00:01 100处记数+1, A再去判断的时候就会漏掉B这个数, 导致A也通过


http://www.kler.cn/a/7902.html

相关文章:

  • ISUP协议视频平台EasyCVR视频设备轨迹回放平台智慧农业视频远程监控管理方案
  • 整理iPhone空间:iphone怎么删除相簿
  • sql专题 之 where和join on
  • 2个word内容合并
  • git没有识别出大写字母改成小写重命名的文件目录
  • 算法学习第一弹——C++基础
  • 【文本分类】《基于提示学习的小样本文本分类方法》
  • usb_cam 相机ROS配置
  • R语言多元数据统计分析在生态环境中的应用
  • Elasticsearch:高级数据类型介绍
  • SpringBoot—Spring Data JPA(2)
  • 苹果配摄像头的HomePod专利获批,语音唤醒苹果Find My功能
  • Spring注解开发详解
  • vue2和vue3的区别
  • 深度学习语义分割篇——FCN原理详解篇
  • 手机怎么投屏到电脑?3个简单的方法教会你!
  • 一本通 3.4.2 图的最短路径
  • FL Studio21中文版本下载及语言切换调整
  • Spring循环依赖
  • LCP 44. 开幕式焰火
  • Chat-GLM 详细部署(GPU显存>=12GB)
  • PMP如何备考,才能一次通过呢?
  • Twitter-高性能hash_map
  • AM62X +FPGA+AD/DA 的TSN工业网关解决方案
  • vue实现轨迹回放(很详细)
  • 最近写的怎样审核不通过