Redis实现滑动窗口限流

常见限流算法

  1. 固定窗口算法

    在固定的时间窗口下进行计数,达到阈值就拒绝请求。固定窗口如果在窗口开始就打满阈值,窗口后半部分进入的请求都会拒绝。

  2. 滑动窗口算法

    在固定窗口的基础上,窗口会随着时间向前推移,可以在时间内平滑控制流量,解决固定窗口出现的突发流量问题。

  3. 漏斗算法

    请求来了先进入漏斗,漏斗以恒定的速率放行请求。

  4. 令牌桶算法

    在令牌桶中,以恒定的速率放入令牌,令牌桶也有一定的容量,如果满了令牌就无法放进去了。拿到令牌的请求通过,并消耗令牌,如果令牌桶中令牌为空,则会丢弃该请求。

redis实现滑动窗口算法

当有请求来的时候记录时间戳,统计窗口内请求的数量时只需要统计redis中记录的数量。可以使用redis中的zset结构来存储。key可以设置为请求的资源名,同时根据限流的对象,往key中加入限流对象信息。比如根据ip限制访问某个资源的流量,可以使用方法名+ip作为key。score设置为时间戳。value则可以根据请求参数等信息生成MD5,或者直接生成UUID来存入,防止并发时多个请求存入的score和value一样导致只存入一个数据。

步骤如下:

  1. 定义时间窗口
  2. 请求到来,丢弃时间窗口之外的数据,ZREMRANGEBYSCORE KEYS[i], -inf, window_start
  3. 判断时间窗口内的请求个数是否达到阈值。ZCARD KEYS[i] 要小于阈值
  4. 如果小于则通过zadd加入,超过则返回不放行

lua脚本:

local window_start = tonumber(ARGV[1])- tonumber(ARGV[2])
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)
local current_requests = redis.call('ZCARD', KEYS[1])
if current_requests < tonumber(ARGV[3]) then
    redis.call('ZADD', KEYS[1], tonumber(ARGV[1]), ARGV[4])
    return 1
else
    return 0
end

java通过注解+切面实现限流

在java中,我们的需求是对资源可以进行多种规则的限流。注解可以定义不同类型的限流,如:全局限流,根据IP限流,根据用户限流。对每种类型的限流可以在一个注解中定义多个限流规则。

整体效果如下:

@RateLimiter(rules = {@RateLimitRule(time = 50,count = 100),@RateLimitRule(time = 20,count = 10)}, type = LimitType.IP)
@RateLimiter(rules = {@RateLimitRule(time = 60,count = 1000)}, type = LimitType.DEFAULT)
public void update(){

}

定义注解

定义了三个注解:

  1. RateLimiter:限流注解
  2. RateLimitRule:限流规则
  3. RateLimiters:存放多个限流注解的容器,为了可以重复使用该注解

RateLimiter:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 支持重复注解
@Repeatable(value = RateLimiters.class)
public @interface RateLimiter {

    /**
     * 限流键前缀
     *
     * @return
     */
    String key() default "rate_limit:";

    /**
     * 限流规则
     *
     * @return
     */
    RateLimitRule[] rules() default {};

    /**
     * 限流类型
     *
     * @return
     */
    LimitType type() default LimitType.DEFAULT;
}

RateLimitRule:

public @interface RateLimitRule {
    /**
     * 时间窗口, 单位秒
     *
     * @return
     */
    int time() default 60;

    /**
     * 允许请求数
     *
     * @return
     */
    int count() default 100;
}

RateLimiters:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiters {
    RateLimiter[] value();
}

改造lua脚本

在实现切面之前,我们需要对lua脚本进行改造。我们的需求对资源可以进行多种规则的限流。根据限流类型和限流规则可以组合出不同的key,比如我们要对某个资源进行以下规则限流:全局限流(60s,1000次; 600s,5000次),根据ip限流(2s,5次)。

根据这些规则我们就需要使用3个zset分别来存放请求记录。并且当三个规则都没达到阈值时才放行请求,否则拒绝请求。

对lua脚本改造,支持多个key。

 
local flag = 1
for i = 1, #KEYS do
    local window_start = tonumber(ARGV[1])- tonumber(ARGV[(i-1)*3+2])
    redis.call('ZREMRANGEBYSCORE', KEYS[i], '-inf', window_start)
    local current_requests = redis.call('ZCARD', KEYS[i])
    if current_requests < tonumber(ARGV[(i-1)*3+3]) then
    else
        flag = 0
    end
end
if flag == 1 then
    for i = 1, #KEYS do
        redis.call('ZADD', KEYS[i], tonumber(ARGV[1]), ARGV[(i-1)*3+4])
    end
end
return flag

定义切面

定义一个切面实现限流逻辑:RateLimiterAspect

首先定义切点,由于我们可以重复使用注解,所以需要把RateLimiter和RateLimiters都定义为切点

@Pointcut("@annotation(com.imgyh.framework.annotation.RateLimiter)")
public void rateLimiter() {
}

@Pointcut("@annotation(com.imgyh.framework.annotation.RateLimiters)")
public void rateLimiters() {
}

在前置通知中实现限流逻辑:

主要流程如下:

  1. 把所有的RateLimiter都拿到,解析出限流规则和限流类型
  2. 根据限流规则和限流类型,获取所有的key和参数,为调用lua脚本做准备
  3. 调用lua脚本,根据返回值判断是否放行请求
// 定义切点之前的操作
@Before("rateLimiter() || rateLimiters()")
public void doBefore(JoinPoint point) {
    try {
        // 从切点获取方法签名
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 获取方法
        Method method = signature.getMethod();
        String name = point.getTarget().getClass().getName() + "." + signature.getName();
        // 获取日志注解
        RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);
        RateLimiters rateLimiters = method.getAnnotation(RateLimiters.class);

        List<RateLimiter> limiters = new ArrayList<>();
        if (ObjectUtils.isNotNull(rateLimiter)) {
            limiters.add(rateLimiter);
        }

        if (ObjectUtils.isNotNull(rateLimiters)) {
            limiters.addAll(Arrays.asList(rateLimiters.value()));
        }

        if (!allowRequest(limiters, name)) {
            throw new ServiceException("访问过于频繁,请稍候再试");
        }

    } catch (ServiceException e) {
        throw e;
    } catch (Exception e) {
        throw new RuntimeException("服务器限流异常,请稍候再试");
    }
}

/**
  * 是否允许请求
  *
  * @param rateLimiters 限流注解
  * @param name         方法全名
  * @return 是否放行
  */
private boolean allowRequest(List<RateLimiter> rateLimiters, String name) {
    List<String> keys = getKeys(rateLimiters, name);
    Object[] args = getArgs(rateLimiters);
    Object res = redisTemplate.execute(limitScript, keys, args);

    return ObjectUtils.isNotNull(res) && (Long) res == 1L;
}

/**
  * 获取限流的键
  *
  * @param rateLimiters 限流注解
  * @param name         方法全名
  * @return
  */
private List<String> getKeys(List<RateLimiter> rateLimiters, String name) {
    List<String> keys = new ArrayList<>();

    for (RateLimiter rateLimiter : rateLimiters) {
        String key = rateLimiter.key();
        RateLimitRule[] rules = rateLimiter.rules();
        LimitType type = rateLimiter.type();

        StringBuilder sb = new StringBuilder();
        sb.append(key).append(name);

        if (LimitType.IP == type) {
            String ipAddr = IpUtils.getIpAddr();
            sb.append("_").append(ipAddr);
        } else if (LimitType.USER == type) {
            Long userId = SecurityUtils.getUserId();
            sb.append("_").append(userId);
        }
        for (RateLimitRule rule : rules) {
            int time = rule.time() * 1000;
            int count = rule.count();
            StringBuilder builder = new StringBuilder(sb);
            builder.append("_").append(time).append("_").append(count);
            keys.add(builder.toString());
        }
    }
    return keys;
}

/**
  * 获取需要的参数
  *
  * @param rateLimiters 限流注解
  * @return
     */
private Object[] getArgs(List<RateLimiter> rateLimiters) {
    List<Object> args = new ArrayList<>();
    args.add(System.currentTimeMillis());
    for (RateLimiter rateLimiter : rateLimiters) {
        RateLimitRule[] rules = rateLimiter.rules();
        for (RateLimitRule rule : rules) {
            int time = rule.time() * 1000;
            int count = rule.count();
            args.add(time);
            args.add(count);
            args.add(IdUtils.fastSimpleUUID());
        }
    }
    return args.toArray();
}

实例demo演示

demo源码仓库:github.com/imgyh/devel…

定义接口,并添加限流注解。

限制对某个用户只能1s中访问2次。对接口整体10s中访问50次,60秒访问100次。

当某个用户一秒钟请求超过两次时,抛出异常。

参考资源

  1. Hollis,《Java面试宝典》
  2. 一文搞懂高频面试题之限流算法,从算法原理到实现,再到对比分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/409074.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

矩阵的导数运算(理解分子布局、分母布局)

矩阵的导数运算(理解分子布局、分母布局) 1、分子布局和分母布局 请思考这样一个问题&#xff0c;一个维度为m的向量y对一个标量x的求导&#xff0c;那么结果也是一个m维的向量&#xff0c;那么这个结果向量是行向量&#xff0c;还是列向量呢&#xff1f; 答案是&#xff1a…

【前端素材】推荐优质医院后台管理系统I-Health平台模板(附源码)

一、需求分析 后台管理系统是一种用于管理和监控网站、应用程序或系统的在线工具。它通常是通过网页界面进行访问和操作&#xff0c;用于管理网站内容、用户权限、数据分析等。后台管理系统是网站或应用程序的控制中心&#xff0c;管理员可以通过后台系统进行各种管理和配置操…

如何用Pytest做性能测试?5个步骤轻松学会!

Pytest其实也是可以做性能测试或者基准测试的。是非常方便的。 可以考虑使用Pytest-benchmark类库进行。 安装pytest-benchmark 首先&#xff0c;确保已经安装了pytest和pytest-benchmark插件。可以使用以下命令安装插件&#xff1a; pip install pytest pytest-benchmark …

无需软件_暂停 Windows更新

按下 WIN 加 R 输入 regedit 进入注册表 点击 HKEY LOCAL-MACHINE 点击 SOFTWARE 点击 Microsoft 点击 WindowsUpdate 点击 UX 点击 Settings 然后空白区点击右键 选择新建&#xff0c;选择项&#xff0c;选择 dword 值 命名为 FlightSettingsMaxPauseDays 基数选择十…

创作纪念日:记录我的成长与收获

机缘 一开始是在我深入学习前端知识的Vue.js框架遇到了一个问题&#xff0c;怎么都解决不了&#xff0c;心烦意乱地来csdn上找解决方法。开心的是真被我找到了&#xff0c;真的很感恩&#xff0c;也意识到在这个平台上分享自己的经验是多么有意义的事情&#xff0c;可能随便的…

技术场景面试题

1.项目中遇到的比较棘手的问题&#xff0c;如何解决的。 第一个方向是设计模式在项目中的应用&#xff0c;遵循一系列的开发原则&#xff0c;设计模式都是前人总结出来的经验&#xff0c;对我们的开发有指导意义。 之前没有用设计模式&#xff0c;所有的登录都糅合在一个业务…

2024年云南事业单位报名流程!明天就开始报名啦,千万不要错过哦

注意啦&#xff01;注意啦&#xff01;2024年云南事业单位报名即将开始&#xff01; ▶️公告已发布&#xff0c;2月26日上午9&#xff1a;00开始报名‼️ 相关时间节点 **报名时间&#xff1a;**2024年2月26日9:00至3月1日18:00 **资格初审时间&#xff1a;**2024年2月26日…

新版Java面试专题视频教程——虚拟机篇①

新版Java面试专题视频教程——虚拟机篇① 1 JVM组成1.1 JVM由那些部分组成&#xff0c;运行流程是什么&#xff1f;1.2 什么是程序计数器&#xff1f;1.3 你能给我详细的介绍Java堆吗?1.3.1 1.7和1.8 堆的区别1.3.2 元空间(MetaSpace)介绍 1.4 什么是虚拟机栈1.4.1 堆和栈的区…

centos7 docker 安装

1.卸载之前docker环境 sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docke…

软件测试人员的基本功包括些什么?

软件测试人员的基本功包括哪些呢&#xff1f;接下来该问题的阐述结构如下&#xff1a; 1、一看软件测试基本流程 2、明确软件测试的基本功有哪些 3、如何牢固掌握这些基本功 软件测试基本流程 上图就是软件测试的基本流程 1&#xff09;需求评审 2&#xff09;计划编写 …

南京观海微电子---AXI总线技术简介——ZYNQ PS和PL的互联技术

1.AXI总线介绍 AXI全称Advanced Extensible Interface&#xff0c;是Xilinx从6系列的FPGA开始引入的一个接口协议&#xff0c;主要描述了主设备和从设备之间的数据传输方式。AXI协议在Xilinx的ZYNQ系列芯片中继续使用&#xff0c;协议版本是AXI4。 ZYNQ为Xilinx推出的首款将高…

PHATGOOSE:使用LoRA Experts创建低成本混合专家模型实现零样本泛化

这篇2月的新论文介绍了Post-Hoc Adaptive Tokenwise Gating Over an Ocean of Specialized Experts (PHATGOOSE)&#xff0c;这是一种通过利用一组专门的PEFT模块(如LoRA)实现零样本泛化的新方法 这个方法冻结整个模型&#xff0c;包括PEFT模块&#xff0c;并为每个模块训练一…

git之分支管理

一.理解分支 我们看下面这张图片&#xff1a; 在版本回退⾥&#xff0c;你已经知道&#xff0c;每次提交&#xff0c;Git都把它们串成⼀条时间线&#xff0c;这条时间线就可以理解为是⼀个分⽀。截⽌到⽬前&#xff0c;只有⼀条时间线&#xff0c;在Git⾥&#xff0c;这个分⽀…

动态规划--状态转移

解码方法 1.题目 2.思路 1&#xff09;我们定义一个数组dp&#xff0c;其中dp[i]表示字符串s的前i个字符的解码方法总数。初始化时&#xff0c;dp[0]为1&#xff0c;因为空字符串有一种解码方式。dp[1]的值取决于第一个字符是否是0&#xff0c;如果不是0&#xff0c;则有一种…

LeetCode234.回文链表

题目 给你一个单链表的头节点 head &#xff0c;请你判断该链表是否为回文链表。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 示例 输入&#xff1a;head [1,2,2,1] 输出&#xff1a;true思路 找到链表的中间节点&#xff1a;可以使用快慢指针…

platform(驱动层+应用层)实现终端和中断开关点灯

设备树文件添加 myplatform{compatible"hqyj,myplatform";interrupt-parent<&gpiof>;interrupts<8 0>,<7 0>,<9 0>;led1-gpio<&gpioe 10 0>;led2-gpio<&gpiof 10 0>;led3-gpio<&gpioe 8 0>;reg<0x123…

锂电池SOC估计 | PyTorch实现基于Basisformer模型的锂电池SOC估计

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 PyTorch实现基于Basisformer模型的锂电池SOC估计 锂电池SOC估计&#xff0c;全新【Basisformer】时间序列预测 1.采用自适应监督自监督对比学习方法学习时序特征&#xff1b; 2.通过双向交叉注意力机制计算历史序列和…

从源码学习static的使用

从源码学习static的使用 前言 ​ static意味静态的&#xff0c;在Java中&#xff0c;主要用来修饰类级别的变量或方法等&#xff0c;被修饰的内容&#xff0c;表示随着类的加载而加载&#xff0c;而不是具体的实例级别。 ​ 具体到static的使用场景&#xff0c;主要有以下用…

java 并发的三大特性

CPU 三级缓存架构 为平衡CPU与主存的处理速度问题&#xff0c;提出在CPU中设置多级缓存机制。 当CPU要读取一个数据时&#xff0c;首先从一级缓存中查找&#xff0c;如果没有找到再从二级缓存中查找&#xff0c;如果还是没有就从三级缓存或内存中查找。 每个核心都含有一套L…

高频面试题整理(一)

文章目录 平台无关性如何实现&#xff1f;JVM如何加载 .class文件&#xff1f;什么是反射?谈谈ClassLoader谈谈类的双亲委派机制类的加载方式Java的内存模型?JVM内存模型-jdk8程序计数器&#xff1a;Java虚拟机栈局部变量表和操作数栈&#xff1a; Java内存模型中堆和栈的区别…