Redis学习4——Redis应用之限流

引言

Redis作为一个内存数据库其读写速度非常快,并且支持原子操作,这使得它非常适合处理频繁的请求,一般情况下,我们会使用Redis作为缓存数据库,但处理做缓存数据库之外,Redis的应用还十分广泛,比如这一节,我们将讲解Redis在限流方面的应用。

通过setnx实现限流

我们通过切面,来获取某给接口在一段时间内的请求次数,当请求次数超过某个值时,抛出限流异常,直接返回,不执行业务逻辑。思路大致如下:

初步实现

我们参照上面的流程,对Redis限流进行实现。首先引入aop切面相关的依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

然后添加一个限流注解类,这个注解有三个属性,maxTimes表示最大访问次数,interval表示限流间隙,unit表示时间的单位,假设配置的值为maxTimes=10, interval=1, unit= TimeUnit.SECONDS,那么表示在1秒内,限制访问次数为10次。

package org.example.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
    // 访问次数
    public int maxTimes() default 1;

    // 间隔时间
    public int interval() default 1;

    // 时间单位
    public TimeUnit unit() default TimeUnit.SECONDS;
}

返回结果类:

package org.example.common;

import lombok.Getter;

import java.io.Serializable;

public class Response <T>  implements Serializable {
    @Getter
    private int code;

    @Getter
    private String msg;

    @Getter
    private T data;

    private Response(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    private Response(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    private Response(ResultCode resultCode) {
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
    }

    private Response(ResultCode resultCode, T data) {
        this.code = resultCode.getCode();
        this.msg = resultCode.getMsg();
        this.data = data;
    }

    public static <T> Response success() {
        return new Response(ResultCode.SUCCESS);
    }

    public static <T> Response success(T data) {
        return new Response(ResultCode.SUCCESS, data);
    }

    public static <T> Response fail() {
        return new Response(ResultCode.FAIL);
    }

    public static <T> Response fail(ResultCode resultCode) {
        return new Response(resultCode);
    }

    public static <T> Response error() {
        return new Response(ResultCode.SERVER_ERROR);
    }

    public static <T> Response error(String msg) {
        return new Response(ResultCode.SERVER_ERROR.getCode(), msg);
    }
}

错误码类,在错误码中,我们添加一个LIMIT_ERROR,表示该接口被限流。

package org.example.common;

public enum ResultCode {
    SUCCESS(200, "操作成功"),

    FAIL(400, "操作失败"),

    SERVER_ERROR(500, "服务器错误"),

    LIMIT_ERROR(400, "限流");

    int code;

    String msg;

    ResultCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return this.code;
    }

    public String getMsg() {
        return this.msg;
    }
}

业务异常类

public class BusinessException extends RuntimeException {
    private ResultCode resultCode;

    public BusinessException(ResultCode resultCode) {
        super(resultCode.getMsg());
        this.resultCode = resultCode;
    }

    public ResultCode getResultCode() {
        return this.resultCode;
    }
}

全局异常处理类,在我们的切面中,如果发现访问次数大于最大访问次数,那么抛出限流异常,由全局异常处理类进行处理,返回对应的结果

package org.example.exception;

import org.example.common.Response;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(value = BusinessException.class)
    public Response handleBusinessException(BusinessException e) {
        return Response.fail(e.getResultCode());
    }

    @ExceptionHandler(value = Exception.class)
    public Response handleException(Exception e) {
        return Response.error(e.getMessage());
    }
}

限流切面类

package org.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.annotations.Limit;
import org.example.common.ResultCode;
import org.example.exception.BusinessException;
import org.example.util.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LimitAspect {
    @Autowired
    private RedisUtils redisUtils;

    @Pointcut("@annotation(org.example.annotations.Limit)")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        String prefixMethod = joinPoint.getSignature().getDeclaringTypeName();
        String fullMethodName = prefixMethod + "." + methodName;
        System.out.println("methodName:" + fullMethodName);

        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            System.out.println("method argument:" + arg);
        }

        // 获取注解参数
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Limit annotation = methodSignature.getMethod().getAnnotation(Limit.class);
        System.out.println(annotation.unit());
        System.out.println(annotation.maxTimes());
        System.out.println(annotation.interval());

        // 获取redis值
        Object key = redisUtils.getKey(fullMethodName);
        if (key != null) {
            Integer redisValue = (Integer) key;
            // 小于限流值
            if (redisValue.compareTo(annotation.maxTimes()) < 0) {
                redisUtils.increment(fullMethodName);
                return;
            }
            // 大于限流值
            throw new BusinessException(ResultCode.LIMIT_ERROR);
        }

        // 获取的值为null, 设置数据到redis中
        redisUtils.addKey(fullMethodName, 1, annotation.interval(), annotation.unit());
    }
}

最后添加一个TestController类,用于进行接口的测试:

package org.example.controller;

import org.example.annotations.Limit;
import org.example.common.Response;
import org.example.common.ResultCode;
import org.example.exception.BusinessException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping(value = "/test")
public class TestController {

    @GetMapping(value = "/hello1")
    @Limit(maxTimes = 10, interval = 100, unit = TimeUnit.SECONDS)
    public Response hello1(@RequestParam(name = "name", defaultValue = "cxy") String name) {
        return Response.success("hello1 success " + name);
    }
}

从上面的接口注解配置中,可以看出,这个接口在100秒内最多访问10次,我们启动项目,访问/test/hello1,前10次的访问结果为:
image.png
第11次时,开始限流了
image.png
这里看起来不是很直观,我们将时间间隙改为2,表示2秒最多由10个请求能执行

@GetMapping(value = "/hello1")
    @Limit(maxTimes = 10, interval = 2, unit = TimeUnit.SECONDS)
    public Response hello1(@RequestParam(name = "name", defaultValue = "cxy") String name) {
        return Response.success("hello1 success " + name);
    }

使用postman进行并发请求,下面的redis限流测试,就是刚才提到的http://localhost:8080/test/hello1?name=cxy这个请求
image.png
执行该并发测试,结果如下:
image.png
这里20个请求中,有10个成功,10个被限流。不过这个postman结果展示不太好,只能一个一个查看结果,这里就不一一展示了。

职责分离

上面的代码,虽然能成功限流,但是有一个问题,就是切面类的beforeAdvice方法中,做的事情太多了,又是解析请求参数、解析注解参数,又是使用查询Redis,进行限流判断,我们应该将限流逻辑的判断,此外,这里使用的是Redis,如果后续我们不使用Redis,换成其他方式进行限流判断的话,需要改很多处代码,因此,这里要做一些优化,包括:
1)定义限流请求类,用于封装访问的方法名、注解信息等内容
2)定义限流处理接口
3)定义Redis限流处理类,通过Redis实现限流处理接口
我们首先定义一个限流请求类,封装限流处理所需要的参数:

package org.example.request;

import lombok.Data;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Data
public class LimitRequest implements Serializable {
    private String methodName;

    private Integer interval;

    private Integer maxTimes;

    private TimeUnit timeUnit;

    private Map<String, Object> extendMap = new HashMap<>();
}

定义限流处理接口

package org.example.limit;

import org.example.request.limit.LimitRequest;

public interface LimitHandler {
    void handleLimit(LimitRequest limitRequest);
}

定义Redis的限流处理类

package org.example.limit;

import org.example.common.ResultCode;
import org.example.exception.BusinessException;
import org.example.request.limit.LimitRequest;
import org.example.util.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class RedisLimitHandler implements LimitHandler{
    @Autowired
    private RedisUtils redisUtils;

    @Override
    public void handleLimit(LimitRequest limitRequest) {
        String methodName = limitRequest.getMethodName();
        // 获取redis值
        Object key = redisUtils.getKey(methodName);
        if (key != null) {
            Integer redisValue = (Integer) key;
            // 小于限流值
            if (redisValue.compareTo(limitRequest.getMaxTimes()) <= 0) {
                redisUtils.increment(methodName);
                return;
            }
            // 大于限流值
            throw new BusinessException(ResultCode.LIMIT_ERROR);
        }

        // 获取的值为null, 设置数据到redis中
        redisUtils.addKey(methodName, 1, limitRequest.getInterval(), limitRequest.getTimeUnit());
    }
}

修改LimitAspect代码,但后续更换限流策略是,只需要修改LimitHandler的bean即可。

package org.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.annotations.Limit;
import org.example.limit.LimitHandler;
import org.example.request.limit.LimitRequest;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
@Aspect
public class LimitAspect {

    @Resource
    private LimitHandler redisLimitHandler;

    @Pointcut("@annotation(org.example.annotations.Limit)")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        LimitRequest limitRequest = convert2LimitRequest(joinPoint);

        redisLimitHandler.handleLimit(limitRequest);
    }

    private LimitRequest convert2LimitRequest(JoinPoint joinPoint) {
        LimitRequest limitRequest = new LimitRequest();

        String methodName = joinPoint.getSignature().getName();
        String prefixMethod = joinPoint.getSignature().getDeclaringTypeName();
        limitRequest.setMethodName(prefixMethod + "." + methodName);

        Object[] args = joinPoint.getArgs();
        limitRequest.getExtendMap().put("args", args);

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Limit annotation = methodSignature.getMethod().getAnnotation(Limit.class);
        limitRequest.setInterval(annotation.interval());
        limitRequest.setMaxTimes(annotation.maxTimes());
        limitRequest.setTimeUnit(annotation.unit());
        return limitRequest;
    }
}

通过Zset实现限流

我们可以将请求打造成一个zset数组,每一次请求进来时,value保持一致,可以用UUID生成,然后score用当前时间戳表示,通过range方法,来获取某个时间范围内,请求的个数,然后根据这个个数与限流值对比,当大于限流值时,进行限流操作。
我们修改RedisLimitHandler代码如下:

 @Override
    public void handleLimit(LimitRequest limitRequest) {
       handleLimitByZSet(limitRequest);
    }

    private void handleLimitByZSet(LimitRequest limitRequest) {
        String methodName = limitRequest.getMethodName();

        long currentTime = System.currentTimeMillis();
        long interval = TimeUnit.MILLISECONDS.convert(limitRequest.getInterval(), limitRequest.getTimeUnit());

        if (redisUtils.hasKey(methodName)) {
            int count = redisUtils.rangeByScore(methodName, Double.valueOf(currentTime - interval), Double.valueOf(currentTime)).size();
            if (count > limitRequest.getMaxTimes()) {
                throw new BusinessException(ResultCode.LIMIT_ERROR);
            }
        }
        redisUtils.addZSet(methodName, UUID.randomUUID().toString(), Double.valueOf(currentTime));
    }

然后添加一个测试类,用于模拟并发场景下的多个请求

package org.example;

import com.alibaba.fastjson.JSONObject;
import org.example.common.Response;
import org.example.common.ResultCode;
import org.example.controller.TestController;
import org.example.exception.BusinessException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

@SpringBootTest
public class RedisLimitTest {
    @Autowired
    private TestController testController;

    @Test
    public void testLimit() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        Callable<Response> callable = () -> {
            try {
                String name = "cxy";
                return testController.hello1(name);
            } catch (BusinessException e) {
                return Response.fail(e.getResultCode());
            }
        };
        List<Future<Response>> futureList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            Future<Response> submit = executorService.submit(callable);
            futureList.add(submit);
        }

        for (Future<Response> future : futureList) {
            System.out.println(JSONObject.toJSONString(future.get()));
        }

    }
}

运行结果如下:
image.png
我们可以看到,这里确实进行限流了,但是,这个限流个数不太对,这是因为可能多个请求都执行到这条代码,获取到同一个值,然后才进行更新。
int count = redisUtils.rangeByScore(methodName, Double.valueOf(currentTime - interval), Double.valueOf(currentTime)).size();
比如有5个请求同时打过来,此时的执行到上面这条代码时,redis中符合范围的刚好有9条,那么这5个请求在进行判断时,都小于限流值,因此都会执行,然后才是更新zset,这个就是并发场景下的问题了。
另外,使用zset还有一个问题,它虽然能达到滑动窗口的效果,但是zset的数据结构会越来越大。

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

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

相关文章

远程服务器 docker XRDP 桌面访问 记录

需求描述: 我现在在远程连接 一台服务器&#xff0c;由于需要实验环境需要GUI 和 桌面系统&#xff0c;但是又想在 docker 中运行。因此&#xff0c;我现在首先需要通过 ssh 连接服务器&#xff0c;然后再服务器中连接 docker. REF: https://github.com/danielguerra69/ubuntu-…

代码随想录day19day20打卡

二叉树 1 二叉树的最大深度和最小深度 最大深度已经学习过了&#xff0c;实质就是递归的去判断左右子节点的深度&#xff0c;然后对其进行返回。 附加两个学习的部分&#xff1a; &#xff08;1&#xff09;使用前序遍历的方法求解 int result; void getdepth(TreeNode* nod…

NXP i.MX8系列平台开发讲解 - 3.11 Linux PCIe设备调试(WIFI模块)

专栏文章目录传送门&#xff1a;返回专栏目录 文章目录 目录 1. WIFI 模块简单介绍 2. 设备驱动原理介绍 3. PCIE WIFI驱动实例分析 3.1 查看设备树 3.2 wifi 设备驱动代码分析 3.3 内核配置选项 4. WIFI驱动调试相关 根据前面对PCIe的讲解&#xff0c;对PCIe的整体都有…

Ansible --- playbook 脚本+inventory 主机清单

一 inventory 主机清单 Inventory支持对主机进行分组&#xff0c;每个组内可以定义多个主机&#xff0c;每个主机都可以定义在任何一个或 多个主机组内。 如果是名称类似的主机&#xff0c;可以使用列表的方式标识各个主机。vim /etc/ansible/hosts[webservers]192.168.10.1…

Flutter弹窗链-顺序弹出对话框

效果 前言 弹窗的顺序执行在App中是一个比较常见的应用场景。比如进入App首页&#xff0c;一系列的弹窗就会弹出。如果不做处理就会导致弹窗堆积的全部弹出&#xff0c;严重影响用户体验。 如果多个弹窗中又有判断逻辑&#xff0c;根据点击后需要弹出另一个弹窗&#xff0c;这…

C++ Primer 总结索引 | 第十四章:重载运算与类型转换

1、C语言定义了 大量运算符 以及 内置类型的自动转换规则 当运算符 被用于 类类型的对象时&#xff0c;C语言允许我们 为其指定新的含义&#xff1b;也能自定义类类型之间的转换规则 例&#xff1a;可以通过下述形式输出两个Sales item的和&#xff1a; cout << item1 …

信息系统安全与对抗-网络侦查技术与网络扫描技术(期末复习)

1、网络拓扑结构在网络攻击中的作用 查明目标网络的拓扑结构&#xff0c;有利于找到目标网络的关键节点&#xff0c;从而提高攻击效率&#xff0c;达到最大攻击效果。 2、网络侦查在网络攻击中的作用 识别潜在目标系统&#xff0c;确认目标系统适合哪种类型的攻击。 3、百度…

jenkins部署服务到windows系统服务器

1、安装openSSH windows默认不支持ssh协议&#xff0c;需要下载安装&#xff0c;主要适用于jenkins传输文件已经执行命令使用 点击查看下载openSSH 2、项目配置 这里简单说说怎么配置&#xff0c;主要解决点就是ssh执行cmd或shell命令时不能开启新窗口导致应用部署失败或者断…

【机器学习系统的构建】从模型开发的过程讲清楚K-Fold 交叉验证 (Cross-Validation)的原理和应用

0、前言 最近在学习集成学习的时候了解到了k折交叉验证&#xff0c;其实在之前学习吴恩达老师的课程中也学过交叉验证&#xff0c;但是当时也不是很明白。这次借着自己的疑问以及网上搜找资料&#xff0c;终于把交叉验证给弄明白了。 在弄清楚前&#xff0c;我有这样几个疑问…

rancher/elemental 构建不可变IOS(一)

一、什么是elemental Elemental 是 Rancher 的一个变种&#xff0c;专注于提供一个更轻量级的 Kubernetes 发行版。它旨在提供简化的部署和管理体验&#xff0c;同时保持 Kubernetes 的灵活性和强大功能。Elemental 通常针对较小的部署场景或资源受限的环境&#xff0c;例如测…

BFS Ekoparty 2022 -- Linux Kernel Exploitation Challenge

前言 昨天一个师傅给了我一道 linux kernel pwn 题目&#xff0c;然后我看了感觉非常有意思&#xff0c;题目也不算难&#xff08;在看了作者的提示下&#xff09;&#xff0c;所以就花时间做了做&#xff0c;在这里简单记录一下。这个题是 BFS Lab 2022 年的一道招聘题&#…

JavaEE技术之MySql高级-搭建主从复制(主从同步原理、一主多从配置)

文章目录 MySQL主从同步1、MySQL主从同步原理2、一主多从配置2.1、准备主服务器2.2、准备从服务器2.3、启动主从同步2.4、实现主从同步2.5、停止和重置2.6、常见问题问题1问题2 MySQL主从同步 1、MySQL主从同步原理 基本原理&#xff1a; slave会从master读取binlog来进行数据…

软件架构的艺术:探索演化之路上的18大黄金原则

实际工作表明&#xff0c;一步到位的设计往往不切实际&#xff0c;而演化原则指导我们逐步优化架构&#xff0c;以灵活响应业务和技术的变化。这不仅降低了技术债务和重构风险&#xff0c;还确保了软件的稳定性和可扩展性。同时&#xff0c;架构的持续演进促进了团队协作&#…

SQL查询语句(二)逻辑运算关键字

上一篇文章中我们提到了条件查询除了一些简单的数学符号之外&#xff0c;还有一些用于条件判断的关键字&#xff0c;如逻辑判断 关键字AND,OR,NOT和范围查找关键字BETWEEN,IN等&#xff1b;下面我们来介绍一些这些关键字的用法以及他们所表达的含义。 目录 逻辑运算关键字 AND…

HarmonyOS实战开发教程-如何开发一个2048游戏

今天为大家分享的是2048小游戏&#xff0c;先看效果图&#xff1a; 这个项目对于新手友友来说可能有一点难度&#xff0c;但是只要坚持看完一定会有收获。因为小编想分享的并不局限于ArkTs语言&#xff0c;而是编程思想。 这个游戏的基本逻辑是初始化一个4乘4的数组&#xff…

【Toritoise SVN】SVN 怎么忽略文件夹下的所有文件但是不忽略文件夹本身

比如&#xff1a;忽略 Assets\StreamingAssets\LocalAsset文件夹下的所有文件但是不忽略LocalAsset这个文件夹 在TortoiseSVN中&#xff0c;你可以通过以下步骤来修改文件夹的svn:ignore属性&#xff1a; 打开Windows资源管理器&#xff0c;导航到你的工作副本中的Assets\Stre…

Python | Leetcode Python题解之第67题二进制求和

题目&#xff1a; 题解&#xff1a; class Solution:def addBinary(self, a, b) -> str:return {0:b}.format(int(a, 2) int(b, 2))

谷歌发布 HEAL 架构,4 步评估医学 AI 工具是否公平

如果把维持健康状态想象成一场赛跑&#xff0c;并不是所有人都能够站在统一起跑线上&#xff0c;有的人能够平稳的跑完全程&#xff0c;有的人即使跌倒也能够在第一时间获得帮助&#xff0c;但是有些人可能因为经济条件、居住地、教育水平、种族或其他因素而面临更多障碍。 「…

新火种AI|挑战谷歌,OpenAI要推出搜索引擎?

作者&#xff1a;一号 编辑&#xff1a;美美 在AI革新的浪潮下&#xff0c;谷歌搜索迎来了越来越多的“挑战者”。 最近&#xff0c;据多家外媒的消息&#xff0c;有知情人士透露&#xff0c;OpenAI正计划上线一款基于ChatGPT的大型产品&#xff0c;将提供一个新的搜索引擎&…

Ansible自动化运维工具 - playbook 剧本编写

一. inventory 主机清单 Inventory 支持对主机进行分组&#xff0c;每个组内可以定义多个主机&#xff0c;每个主机都可以定义在任何一个或多个主机组内。 1.1 inventory 中的变量含义 Inventory 变量名 含义ansible_hostansible连接节点时的IP地址ansible_port连接对方…