Redis的自增也能实现滑动窗口限流?

news2024/11/20 1:29:50

文章目录

  • 限流核心原理以及代码
  • 基于Spring切面实现的注解版本

限流是大家开发之路上一定会遇到的需求。比如:限制一定时间内,接口请求请求频率;一定时间内用户发言、评论次数等等,类似于滑动窗口算法。这里分享一份拿来即用的代码,一起看看如何利用常见的 Redis 实现一个实用并且原理简单的限流器。

限流核心原理以及代码

这个限流器的原理是使用 Redis 的incr命令来累计次数,key 的过期时间作为时间滑动窗口来实现。比如限制每5秒最多请求10次,那么就将 key 的过期时间设置为5秒,每次执行前对这个 key 自增,5秒内的次数将累计到这一个 key 上,如果自增的结果没有超过10次,代表没有被限流。5秒过后 key 将被 Redis 清除,后续次数将重新累计。

这里大家需要了解下incr使用的一些细节。incr每次执行都是将 key 的值自增1,并返回自增后的结果,比如对key=1执行incr结果为2;如果 key 不存在,将设置这个 key 值为1,返回结果自然也是1,并且这个 key 是没有过期时间的。

Redis 的incr不能在自增的同时设置过期时间,这就意味着自增和设置过期时间要分两步做,在第一次incr完成之后,紧接着使用expire指令来给这个 key 设置过期时间。非原子方式会带来并发问题,如果incr成功,而expire失败将导致生成了一个永不过期的 key,次数一直累计到最大值,永远进入限流状态。这个问题我们可以用个兜底逻辑来解决,在incr前获取这个 key 的过期时间,如果没有那就删掉。

看到这,有了解过 Redis lua 脚本的同学可能会提出,既然这么麻烦,**为何不用 lua 脚本自己实现一个自增且同时能够同时设置过期时间的功能?**这个思路很棒,代码量不大且 Redis 也是完全可以支持的。但是在大点的公司,运维可能会禁止开发使用 lua 这种扩展方式,Redis 只有一个主线程执行执行命令,如果脚本中的逻辑执行时间过长将导致后续指令排队等待,它们响应时间自然也会变长,这种不可控的风险运维肯定不愿意承担。当然如果公司允许,并且有其他手段可以控制这个风险,lua 实现还是非常可行的。

**为何不直接使用JDK实现而要借助中间件?**因为实现出来只能在当前进程有有效,集群情况下不能累计到一起。

下面是具体代码,可以直接使用,代码关键处有详细的注释:

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.NonNull;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 用Redis实现的限流器,用于限制方法或者接口请求频率。比如:限制接口每秒请求次数;某个用户请求接口的次数,属于滑动窗口算法。
 * 核心方法是 {@link #acquire(RedisTemplate, String, long, long)}
 */
public abstract class RedisIncrLimiter {

    /**
     * 限制每秒次数,参考 {@link #acquire(RedisTemplate, String, long, long)}
     */
    public static boolean acquireLimitPerSecond(@NonNull RedisTemplate<String, String> redisTemplate,
                                                @NonNull String limiterKey, long maxTimes) {
        return acquire(redisTemplate, limiterKey, 1, maxTimes);
    }

    /**
     * 限制每分钟次数,参考 {@link #acquire(RedisTemplate, String, long, long)}
     */
    public static boolean acquireLimitPerMinute(@NonNull RedisTemplate<String, String> redisTemplate,
                                                @NonNull String limiterKey, long maxTimes) {
        return acquire(redisTemplate, limiterKey, 60, maxTimes);
    }

    /**
     * 限制每小时次数,参考 {@link #acquire(RedisTemplate, String, long, long)}
     */
    public static boolean acquireLimitPerHour(@NonNull RedisTemplate<String, String> redisTemplate,
                                              @NonNull String limiterKey, long maxTimes) {
        return acquire(redisTemplate, limiterKey, 3600, maxTimes);
    }

    /**
     * 限制每天次数,参考 {@link #acquire(RedisTemplate, String, long, long)}
     */
    public static boolean acquireLimitPerDay(@NonNull RedisTemplate<String, String> redisTemplate,
                                             @NonNull String limiterKey, long maxTimes) {
        return acquire(redisTemplate, limiterKey, 86400, maxTimes);
    }

    /**
     * 执行限流逻辑前,调用这个方法获取一个令牌,如果返回 true 代表没被限流,可以执行。比如:
     * <pre>{@code
     * // 限制每秒最多发10次消息
     * if (RedisIncrLimiter.acquire(redisTemplate, "sendMessage", 1, 10)) {
     *     // 发消息
     * } else {
     *     // 被限流后的操作
     * }
     * }</pre>
     * 如果限流粒度是用户级,可以将用户的ID或者唯一身份标识加到限流Key中。<br>
     * 这个也是限流核心方法,利用 Redis incr 命令累计次数,KEY过期时间作为时间窗口实现。<br>
     * 相同的限流KEY、时间窗口和最大次数才会累计到一起,三个参数任一不一致会分开累计,
     * 参考{@link #buildFinalLimiterKey(String, long, long)}
     *
     * @param redisTemplate    redisTemplate
     * @param limiterKey       限流Key(代表限流逻辑的字符串)
     * @param timeWindowSecond 时间窗口
     * @param maxTimes         时间窗口内最大次数
     * @return true-没有被限流
     */
    public static boolean acquire(@NonNull RedisTemplate<String, String> redisTemplate,
                                  @NonNull String limiterKey, long timeWindowSecond, long maxTimes) {
        limiterKey = buildFinalLimiterKey(limiterKey, timeWindowSecond, maxTimes);

        /*
        如果异常情况下产生了没有过期时间的KEY,将导致次数不断累积到最大值(被限流)而无法解除。
        这个兜底操作就是为了避免这个问题,清除没有过期时间的KEY
         */
        Long ttl = redisTemplate.getExpire(limiterKey);
        if (ttl == null || ttl == -1L) {
            redisTemplate.delete(limiterKey);
            return true;
        }

        Long incr = redisTemplate.opsForValue().increment(limiterKey);
        Objects.requireNonNull(incr);

        // 在第一次请求的时候设置过期时间(时间窗口)
        if (incr == 1L) {
            redisTemplate.expire(limiterKey, timeWindowSecond, TimeUnit.SECONDS);
        }

        return incr <= maxTimes;
    }

    /**
     * @param limiterKey       限流Key
     * @param timeWindowSecond 时间窗口
     * @param maxTimes         时间窗口内最大次数
     * @return 构建最终的限流 Redis Key,格式为:限流Key:时间窗口:最多次数
     */
    private static String buildFinalLimiterKey(String limiterKey, long timeWindowSecond, long maxTimes) {
        return limiterKey + ":" + timeWindowSecond + ":" + maxTimes;
    }
}

基于Spring切面实现的注解版本

注解版使用起来比较方便,只需要在限流的方法上指定时间三个关键的参数就行,底层逻辑还是上面的代码。比如:

// 每5秒最多10次
@RedisIncrLimit(limiterKey = "test", timeWindowSecond = 5L, maxTimes = 10L)
public String test() {
	  return "ok";
}

RedisIncrLimit只用来标记限流方法,接收限流参数。

import java.lang.annotation.*;

/**
 * {@link RedisIncrLimiter} 注解版
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisIncrLimit {

    /**
     * @return 限流KEY
     */
    String limiterKey();

    /**
     * @return 时间窗口
     */
    long timeWindowSecond();

    /**
     * @return 时间窗口内最大次数
     */
    long maxTimes();
}

下面切面逻辑doBefore()会在加了RedisIncrLimit注解的方法前执行,先判断是否被限流。

import javax.annotation.Resource;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class RedisLimiterAspect {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Pointcut("@annotation(redisLimit)")
    public void pointcut(RedisIncrLimit redisLimit) {
    }

    @Before("pointcut(redisLimit)")
    public void doBefore(RedisIncrLimit redisLimit) {
        if (!RedisIncrLimiter.acquire(
                redisTemplate, redisLimit.limiterKey(), redisLimit.timeWindowSecond(), redisLimit.maxTimes())) {
            throw new IllegalStateException("rate limit");
        }
    }
}

以上是Redis限流器的全部内容,微信号搜索【wybqbx】或者扫描二维码关注公众号,里面有更多的分享,欢迎大家交流提问

image-20221227223233329

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

buildroot构建hisi平台根文件系统和工具链

buildroot构建hisi平台根文件系统和工具链 前面使用了arm-hisiv300-linux 工具链来作为Buildroot的外部工具链进行编译&#xff0c;然后遇到了很多编译问题。 https://blog.csdn.net/duapple/article/details/128516133?spm1001.2014.3001.5501 这里不使用hisi的工具链&…

Seata简介

小结&#xff1a; nacos 【name server】&#xff1a;注册中心&#xff0c;解决服务的注册与发现 nacos【config】&#xff1a;配置中心&#xff0c;微服务配置文件的中心化管理&#xff0c;同时配置信息的动态刷新 Ribbon&#xff1a;客户端负载均衡器&#xff0c;解决微服务集…

C++之引用类型,深浅拷贝构造

引用类型&#xff1a;给内存段取别名。 int m 10; //引用&#xff0c;给内存段取别名&#xff0c;所以需要给他一段内存段&#xff0c;而不只是声明。 int& n m;//不是赋值的意思&#xff0c;是别名的意思 想要在被调函数中修改主调函数中定义的变量的值时&#xff0c;…

小程序用户头像昵称获取规则调整与之对应调式策略、新API接口的bug

目录 调整时间 调整背景 调整说明 开发者与之对应的debug策略 1.button里面包含一个image&#xff0c;这种包含关系 2.然后我们可以看到官方给出的是用button组件中的open-type属性并且给到了一个chooseAvatar值&#xff01; 3.我们会发现光放给我们了一个“配置好的”命…

CSAPP Cache Lab

CSAPP Cache Lab 本实验将帮助您了解缓存存储器对 C 语言性能的影响程式。实验室由两部分组成。 在第一部分中&#xff0c;您将编写一个小的 C 程序&#xff08;大约 200-300 行&#xff09;模拟高速缓存的行为。 在第二部分中&#xff0c;您将优化一个小型矩阵转置函数&#…

NoMachine出现 The session negotiation failed的解决方案及踩坑总结

问题情况&#xff1a;我A电脑输入用户名和密码可以远程B电脑&#xff0c;B电脑输入用户名密码就是登录不上A电脑。 B电脑上密码是用的账户密码&#xff08;就是图标是一把钥匙的那个&#xff09;。 A电脑上的密码是用的PIN密码&#xff08;Win11系统推荐的那个&#xff09;。 通…

ArcGIS基础实验操作100例--实验37线要素生成规则或随机采样点

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验37 线要素生成规则或随机采样点 目录 一、实验背景 二、实验数据 三、实验步骤 &…

小程序安全设置的经验分享

一、小程序框架概述 在第一部分小程序框架概述中,将介绍小程序抽象框架、小程序调用框架和小程序初始化流程。下面让大白来逐一介绍。 1、小程序抽象框架 1.1视图层 包含WXML、WXSS和页面视图组件。 WXML是一种类似XML格式的语言,支持数据绑定、条件渲染、列表渲染、自定…

零入门容器云网络-10:基于golang编程netlink包方式操作tun设备

已发表的技术专栏&#xff08;订阅即可观看所有专栏&#xff09; 0  grpc-go、protobuf、multus-cni 技术专栏 总入口 1  grpc-go 源码剖析与实战  文章目录 2  Protobuf介绍与实战 图文专栏  文章目录 3  multus-cni   文章目录(k8s多网络实现方案) 4  gr…

【Linux】静态库和共享库

目录 库是什么 静态库和共享库 库的链接 优缺点 查看使用的库 制作库 制作静态库 静态库的使用 制作共享库 共享库的使用 静态库和共享库的区别 库是什么 库就是预先编译好的方法的集合 .h中是库函数的声明&#xff0c;库函数的实现在库中&#xff0c;如&#xff…

ChatGPT上线了!我在2023年1月2日这一天用上了它!百问百答!我只能说(真NB)算法工程师可以不用百度/Google了!

目录:问答结果1、你有什么nlp算法&#xff1f;2、平台终端3、如何训练深度学习模型&#xff1f;4、如何压缩nlp模型&#xff1f;5、bert模型有哪些用途&#xff1f;6、你知道汽车座舱吗&#xff1f;7、知识图谱有什么用途&#xff1f;8、能给一个构建知识图谱的案例吗&#xff…

S32K144—基于MBD的BLDC六步换相算法

可以简单分为六个功能区域&#xff1a; 1、全局变量 全局变量的定义是建模过程中遇到的第一个难点&#xff0c;因为它涉及到我们软件开发中最基础的东西——数据类型定义&#xff08;Data Types Definition&#xff09;。 在 Simulink 中可以通过 Bus Editor 构建自定义数据类…

【 shell 编程 】第5篇 文本编辑三剑客

文本编辑三剑客 文章目录文本编辑三剑客一、正则表达式1.基本正则表达式元字符2.拓展正则表达式元字符二、grep1.grep2.egrep3.fgrep三、sed四、awk一、正则表达式 1、简介&#xff1a;正则表达式是对字符串&#xff08;包括普通字符&#xff08;例如&#xff0c;a 到 z 之间的…

点云算法-提取kitti路面点云

目录 一、ransac原理 二、ransac 地面分割原理 三、ransac常见应用 四、代码 五、截图 六、总结 一、ransac原理 RANSAC是“random sample consensus&#xff08;随机抽样一致&#xff09;”的缩写。它可以从一组包含“局外点”的观测数据集中&#xff0c;通过迭代方式估…

【408篇】C语言笔记-第二十一章(汇编语言)

文章目录第一节&#xff1a;汇编指令格式讲解1. 汇编指令格式2. 生成汇编方法第二节&#xff1a;汇编常用指令讲解1. 相关寄存器2. 常用指令3. 条件码第三节&#xff1a;各种变量赋值汇编实战1. 各种变量赋值汇编实战解析第四节&#xff1a;选择循环汇编实战1. 选择循环汇编实战…

基于ssm+mysql+jsp实现歇后语管理系统

基于ssmmysqljsp实现歇后语管理系统一、系统介绍二、系统展示1.歇后语大全2.歇后语排行榜3.歇后语管理4.用户管理三、其它系统四、获取源码一、系统介绍 本系统实现了 普通用户&#xff1a;歇后语大全、歇后语排行榜、歇后语管理 管理员用户&#xff1a;歇后语大全、歇后语排行…

一键替换Markdown文件的字体样式

功能说明 一键替换MD文件的字体样式&#xff1a;加粗字体—>橙色不加粗 也可以针对其它样式做切换&#xff0c;源码就一个demo类&#xff0c;修改正则匹配相关变量即可&#xff1b; 环境要求&#xff1a; windows jdk1.8 工具下载 百度网盘&#xff1a;提取码: ae16 …

week9

T1【深基18.例3】查找文献 题目描述 小K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个&#xff08;也有可能没有&#xff09;参考文献的链接指向别的博客文章。小K 求知欲旺盛&#xff0c;如果他看了某篇文章&#xff0c;那么他一定会去看这篇文章的参考文献&#xff0…

7.1 定义抽象数据类型

文章目录定义改进的Sales_data 类定义成员函数引入this指针引入const成员函数类作用域和成员函数在类的外部定义成员函数定义返回this的函数类的静态成员定义类相关的非成员函数构造函数合成的默认构造函数某些类不能依赖合成的默认构造函数定义构造函数拷贝 赋值和析构某些类不…

11.2、基于Django4的可重用、用户注册和登录系统搭建(优化)

文章目录前端界面设计与优化完善登录界面的视图函数session会话和登出的视图函数将当前代码推送至Gitee添加图片验证码前端界面设计与优化 使用CSS框架 Bootstrap4&#xff0c;Bootstrap将CSS样式和JS都封装好了&#xff0c;可以直接使用。 下面使用的Bootstrap模板来自官方文…