基于Redis+AOP+Lua脚本实现一个服务器限流机制

embedded/2025/3/17 16:06:38/

后端某些接口在高并发的压力下往往会导致性能的严重下降,为了维持我们后端服务型的高性能和高可用,我们往往可以对某些接口或某些用户去设计限流机制,控制这些热点接口的访问量,我这里利用Redis的高性能优势,并整合AOP编程和引入Lua限流脚本在SpringBoot中对任意接口或某些用户实现了访问量限流的机制,其中,我这里给出了三种限流机制:用户,IP地址,全局限流

1.定义限流方式

 /*** 限流类型* @Author GuihaoLv*/
public enum LimitType {/*** 默认策略全局限流*/DEFAULT,/*** 根据请求者IP进行限流*/IP,/*** 根据请求者的用户ID进行限流*/USER,/*** 根据请求者的部门进行限流*/DEPT,
}

2.引入AOP,自定义限流注解和限流处理的切面类

/*** 限流注解* @Author GuihaoLv*/
//实例 @RateLimiter(time = 60, count = 5, limitType = LimitType.IP) 效果:同一IP 60秒内最多允许5次登录尝试。
@Target(ElementType.METHOD) //表示该注解仅能标注在方法上,用于对具体方法进行限流控制。
@Retention(RetentionPolicy.RUNTIME) //注解在运行时保留,可通过反射机制读取注解信息,实现动态限流逻辑。
@Documented //注解信息会包含在生成的 JavaDoc 中
public @interface RateLimiter {/*** 限流key*/public String key() default RedisConstant.RATE_LIMIT_KEY;/*** 限流时间,单位秒*/public int time() default 60;/*** 限流次数*/public int count() default 100;/*** 限流类型*/public LimitType limitType() default LimitType.DEFAULT;
}
/*** 限流处理切面* @Author GuihaoLv*/
@Aspect
@Component
//确保仅当配置项 spring.cache.type=redis 时,切面才会生效
@ConditionalOnProperty(prefix = "spring.cache", name = { "type" }, havingValue = "redis", matchIfMissing = false)
public class RateLimiterAspect
{private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);private RedisTemplate<Object, Object> redisTemplate; //redis 操作模板,用于执行 Lua 脚本和 Redis 命令private RedisScript<Long> limitScript; //限流核心逻辑的 Lua 脚本@Autowiredpublic void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate){this.redisTemplate = redisTemplate;}@Autowiredpublic void setLimitScript(RedisScript<Long> limitScript){this.limitScript = limitScript;}@Autowiredprivate ObjectMapper jacksonObjectMapper;@Before("@annotation(rateLimiter)")public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable{//1. 获取注解参数int time = rateLimiter.time(); //时间窗口(秒)int count = rateLimiter.count();// 允许的请求次数//2. 生成唯一限流 KeyString combineKey = getCombineKey(rateLimiter, point);List<Object> keys = Collections.singletonList(combineKey);try{//3. 以key为参数执行 Lua 脚本(原子性操作) keys:Redis 存储的 Key//Lua脚本会检查Key是否存在,如果不存在则创建并设置过期时间,如果存在则递增计数器。Long number = redisTemplate.execute(limitScript, keys, count, time);if (StringUtils.isEmpty(number) || number.intValue() > count){throw new Exception("访问过于频繁,请稍候再试");}log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);} catch (RuntimeException e){throw new RuntimeException("服务器限流异常,请稍候再试");} catch (Exception e){throw e;}}//IP + 类名 + 方法名 的拼接方式,确保不同场景的Key不冲突。//Key结构清晰,便于调试和监控(如通过Redis直接查看计数器)。public String getCombineKey(RateLimiter rateLimiter, JoinPoint point){StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());switch (rateLimiter.limitType()) {case IP:try {limitByIp(stringBuffer);} catch (IOException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}break;case USER:limitByUser(stringBuffer);break;case DEFAULT:limitByDefault(stringBuffer,point);break;}return stringBuffer.toString();}/*** 按IP限流* @param stringBuffer*/private final HttpClient httpClient = HttpClient.newHttpClient();private void limitByIp(StringBuffer stringBuffer) throws IOException, InterruptedException {String[] services = {"https://api.ipify.org","https://icanhazip.com"};for (String serviceUrl : services) {HttpRequest request = HttpRequest.newBuilder().uri(URI.create(serviceUrl)).GET().build();HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());String ip = response.body().trim();stringBuffer.append(":ip:").append(ip);}}/*** 全局限流* @param stringBuffer* @param point*/private void limitByDefault(StringBuffer stringBuffer, JoinPoint point) {//按方法限流:拼接类名 + 方法名MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();Class<?> targetClass = method.getDeclaringClass();stringBuffer.append(targetClass.getName()).append("-").append(method.getName());}/*** 按用户限流* @param stringBuffer*/private void limitByUser(StringBuffer stringBuffer) {//获取当前用户String userSubject = UserThreadLocal.getSubject();User user=new User();try {user = jacksonObjectMapper.readValue(userSubject, User.class);} catch (JsonProcessingException e) {throw new RuntimeException("无法获取当前用户");}stringBuffer.append(":user:").append(user.getId());}}

3. 在Redis的配置类中整合Lua语言自定义限流脚本的执行

/*** 限流脚本定义* @return*/
@Bean
public DefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(limitScriptText()); //加载Lua脚本redisScript.setResultType(Long.class); // 返回类型为Longreturn redisScript;
}/*** 限流脚本*/
private String limitScriptText() {return "local key = KEYS[1]\n" +"local count = tonumber(ARGV[1])\n" +"local time = tonumber(ARGV[2])\n" +"local current = redis.call('get', key);\n" +"if current and tonumber(current) > count then\n" +"    return tonumber(current);\n" +"end\n" +"current = redis.call('incr', key)\n" +"if tonumber(current) == 1 then\n" +"    redis.call('expire', key, time)\n" +"end\n" +"return tonumber(current);";
}

4. 限流注解的使用

案例1:登录接口IP限流
@RateLimiter( key = "login_attempt", time = 300, // 5分钟 count = 5, 
limitType = LimitType.IP ) 
@PostMapping("/login") 
public Response login(@RequestBody LoginDTO dto) { // 登录逻辑 }
案例2:API用户维度限流

@RateLimiter( key = "api_v1:data_export", time = 3600, // 1小时 count = 10,

limitType = LimitType.USER )

@GetMapping("/export")

public void exportData() { // 数据导出逻辑 }

这样就能对上述接口实现对应的限流机制了


http://www.ppmy.cn/embedded/173378.html

相关文章

Spring Boot 核心知识点深度详解:自动化配置 (Auto-configuration) - 解锁 Spring Boot 的 “魔法”

Spring Boot 核心知识点深度详解&#xff1a;自动化配置 (Auto-configuration) - 解锁 Spring Boot 的 “魔法” ✨ 自动化配置 (Auto-configuration) 是 Spring Boot 最核心的特性之一&#xff0c;也是它能够大幅简化 Spring 应用开发的关键所在。 它让 Spring Boot 应用能够…

量子计算 × 虚拟现实:未来科技的双剑合璧

量子计算 虚拟现实&#xff1a;未来科技的双剑合璧 前言&#xff1a;当量子计算遇上虚拟现实 虚拟现实&#xff08;VR&#xff09;已经从游戏娱乐逐步渗透到医疗、教育、工业仿真等领域。然而&#xff0c;当前 VR 依然面临诸多挑战&#xff0c;如高计算需求、实时渲染延迟、…

ModelScope推理QwQ32B

文章目录 ModelScope推理QwQ32Bmodel_scope下载QwQ32BModelScope 调用QwQ-32B ModelScope推理QwQ32B 以下载 qwq32b 为例子 需要安装的 python 包 transformers4.49.0 accelerate>0.26.0 torch2.4.1 triton3.0.0 safetensors0.4.5可以使用 conda 创建一个虚拟环境安装 cond…

c#:使用串口通讯实现数据的发送和接收

串口通讯&#xff08;Serial Communication&#xff09;是一种常见的硬件设备与计算机之间的数据传输方式&#xff0c;广泛应用于工业控制、嵌入式系统、传感器数据采集等领域。本文将详细介绍如何使用C#实现基于串口通讯的数据发送和接收&#xff0c;并结合代码示例解析其实现…

在使用element-ui时表单的表头在切换页面时第一次进入页面容易是白色字体解决方法

在里面添加:header-cell-style"{ color: black }" <el-table :data"tableData" style"width: 100%" height"250" :header-cell-style"{ color: black }" ></el-table> 正确代码是 <templat…

贪吃蛇小游戏-简单开发版

一、需求 本项目旨在开发一个经典的贪吃蛇游戏&#xff0c;用户可以通过键盘控制蛇的移动方向&#xff0c;让蛇吃掉随机出现在游戏区域内的食物&#xff0c;每吃掉一个食物&#xff0c;蛇的身体长度就会增加&#xff0c;同时得分也会相应提高。游戏结束的条件为蛇撞到游戏区域的…

【3D视觉学习笔记2】摄像机的标定、畸变的建模、2D/3D变换

本系列笔记是北邮鲁老师三维重建课程笔记&#xff0c;视频可在B站找到。 1. 摄像机的标定 摄像机标定的过程就是从1张或者多张图片中求解相机的内外参数的过程。 根据上一节的知识&#xff0c;针孔摄像机模型的世界坐标系到成像平面的映射关系为 p K [ R , T ] P p K[R,T]…

SQL与NoSQL的区别

以下是SQL与NoSQL数据库的详细对比&#xff0c;涵盖核心特性、适用场景及技术选型建议&#xff1a; 一、核心区别对比 特性SQL&#xff08;关系型数据库&#xff09;NoSQL&#xff08;非关系型数据库&#xff09;数据模型基于表格&#xff0c;严格预定义模式&#xff08;Schem…