SpringBoot实现限流注解

news/2024/10/18 7:49:04/

SpringBoot实现限流注解

在高并发系统中,保护系统的三种方式分别为:缓存,降级和限流。

限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待

1、限流类型枚举类

/*** 限流类型* @author ss_419*/public enum LimitType {/*** 默认的限流策略,针对某一个接口进行限流*/DEFAULT,/*** 针对某一个IP进行限流*/IP}

2、自定义限流注解

/*** @author ss_419*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {/*** 限流的 key,主要是指前缀* @return*/String key() default "rate_limit:";/*** 在时间窗内的限流次数* @return*/int count() default 100;/*** 限流类型* @return*/LimitType limitType() default LimitType.DEFAULT;/*** 限流时间窗* @return*/int time() default 60;
}

3、限流lua脚本

1、由于我们使用 Redis 进行限流,我们需要引入 Redis 的 maven 依赖,同时需要引入 aop 的依赖

<!-- aop依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redis依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置redis以及lua脚本

@Configuration
public class RedisConfig {@BeanRedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);template.setKeySerializer(jackson2JsonRedisSerializer);template.setValueSerializer(jackson2JsonRedisSerializer);template.setHashKeySerializer(jackson2JsonRedisSerializer);template.setHashValueSerializer(jackson2JsonRedisSerializer);return template;}/*** 读取lua脚本* @return*/@BeanDefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> script = new DefaultRedisScript<>();script.setResultType(Long.class);script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua")));return script;}
}

通过 Lua 脚本,根据 Redis 中缓存的键值判断限流时间(也是 key 的过期时间)内,访问次数是否超出了限流次数,没超出则访问次数 +1,返回 true,超出了则返回 false。
limit.lua:

local key = KEYS[1]
local time = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
local current = redis.call('get', key)
if current and tonumber(current) > count thenreturn tonumber(current)
end
current = redis.call('incr', key)
if tonumber(current) == 1 thenredis.call('expire', key, time)
end
return tonumber(current)

4、限流切面处理类

1、使用我们刚刚的 Lua 脚本判断是否超出了限流次数,超出了限流次数后返回一个自定义异常,然后在全局异常中去捕捉异常,返回 JSON 数据。

2、根据注解参数,判断限流类型,拼接缓存 key 值

package org.pp.ratelimiter.aspectj;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.pp.ratelimiter.annotation.RateLimiter;
import org.pp.ratelimiter.enums.LimitType;
import org.pp.ratelimiter.exception.RateLimitException;
import org.pp.ratelimiter.utils.IpUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.lang.reflect.Method;
import java.util.Collections;@Aspect
@Component
public class RateLimiterAspect {private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class);@AutowiredRedisTemplate<Object, Object> redisTemplate;@AutowiredRedisScript<Long> redisScript;@Before("@annotation(rateLimiter)")public void before(JoinPoint jp, RateLimiter rateLimiter) throws RateLimitException {int time = rateLimiter.time();int count = rateLimiter.count();String combineKey = getCombineKey(rateLimiter, jp);try {Long number = redisTemplate.execute(redisScript, Collections.singletonList(combineKey), time, count);if (number == null || number.intValue() > count) {//超过限流阈值logger.info("当前接口以达到最大限流次数");throw new RateLimitException("访问过于频繁,请稍后访问");}logger.info("一个时间窗内请求次数:{},当前请求次数:{},缓存的 key 为 {}", count, number, combineKey);} catch (Exception e) {throw e;}}/*** 这个 key 其实就是接口调用次数缓存在 redis 的 key* rate_limit:11.11.11.11-org.javaboy.ratelimit.controller.HelloController-hello* rate_limit:org.javaboy.ratelimit.controller.HelloController-hello* @param rateLimiter* @param jp* @return*/private String getCombineKey(RateLimiter rateLimiter, JoinPoint jp) {StringBuffer key = new StringBuffer(rateLimiter.key());if (rateLimiter.limitType() == LimitType.IP) {key.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())).append("-");}MethodSignature signature = (MethodSignature) jp.getSignature();Method method = signature.getMethod();key.append(method.getDeclaringClass().getName()).append("-").append(method.getName());return key.toString();}
}

5、使用与测试

@RestController
public class HelloController {/*** 限流 10 秒之内,这个接口可以访问3次* @return*/@GetMapping("/hello")@RateLimiter(time = 10,count = 3)public Map<String, Object> hello() {Map<String, Object> map = new HashMap<>();map.put("status", 200);map.put("message", "Hello RateLimiter");return map;}}

十秒之内访问次数超过3次就会报异常
image

redis中的数据,每一次访问都加1
image
当访问次数超过3,则进行限流操作
image


http://www.ppmy.cn/news/80300.html

相关文章

基于中文在线文档的Polars工具介绍

Polars学习简介 Polars是一个能够提取&#xff08;Extract&#xff09;、转换&#xff08;Transform&#xff09;与加载&#xff08;Load&#xff09;大规模数据集的工具&#xff08;快速多线程、单指令多数据流、延迟/即时执行、查询优化、混合流等&#xff09;。根据官方开发…

网络安全的学习路线是怎么样的?

在众多高大上的学习路线指导中&#xff0c;尝试做一股清流&#xff0c;把要讲清楚的都讲清楚&#xff0c;该学些什么&#xff0c;学到哪个程度进入到下一阶段的学习这些才是最重要的。 在学习之前首先要做好学习的系统规划&#xff1a; 1.目前市场需求主流的岗位里&#xff0…

Python3 命名空间和作用域

在Python中&#xff0c;命名空间&#xff08;Namespace&#xff09;是一个用于存储变量名称和其对应对象的系统。它提供了一种在程序中组织和访问变量的方式&#xff0c;以防止命名冲突并提供代码模块化的能力。 Python中的命名空间可以被视为一个字典&#xff0c;其中变量名称…

Android 系统内的守护进程 - main类服务(3) : installd

声明 只要是操作系统,不用说的就是其中肯定会运行着一些很多守护进程(daemon)来完成很多杂乱的工作。通过系统中的init.rc文件也可以看出来,其中每个service中就包含着系统后台服务进程。而这些服务被分为:core类服务(adbd/servicemanager/healthd/lmkd/logd/vold)和mai…

今日单词|长期主义 (Day 1)

aquifier n.含水层 replenishsupplement vt.补充 oxytocin n.催产素 heyday n.全盛时期 In its heyday, the company ran trains every fifteen minutes. desalination n. desalinate salination salinate salt n. Its too salty. savory. a.令人愉快的、可口的 savor all …

ISO证书“带标”与“不带标”的区别是什么?

ISO9001质量管理体系认证是企业产品获得“通行绿卡”的最直接最有效的途径。 通过认证在打破贸易壁垒&#xff0c;提高产品知名度&#xff0c;降低生产成本&#xff0c;提高经济效益&#xff0c;维护消费者权益&#xff0c;减少重复审核负担等方面的作用越来越为企业界所共知。…

计算机网络实验(ensp)-实验10:三层交换机实现VLAN间路由

目录 实验报告&#xff1a; 实验操作 1.建立网络拓扑图并开启设备 2.配置主机 1.打开PC机 配置IP地址和子网掩码 2.配置完成后点击“应用”退出 3.重复步骤1和2配置每台PC 3.配置交换机VLAN 1.点开交换机 2.输入命名&#xff1a;sys 从用户视图切换到系统视图…

【系统移植】uboot 通过 NFS 加载根文件系统(一) —— 网络环境配置

前面试过了SD卡烧录根文件系统&#xff0c;然后借助环境变量 bootargs 来加载SD卡中的根文件系统&#xff0c;但是实际开发需要经常对文件做增删改等操作&#xff0c;所以将根文件系统放在SD上不大合适。 因此&#xff0c;最常用的做法是把根文件系统放在NFS服务端&#xff08…