springboot+redis+lua脚本实现滑动窗口限流

server/2024/11/28 0:47:06/

1 限流

为了维护系统稳定性和防止DDoS攻击,需要对系统请求量进行限制。

2 滑动窗口

限流方式有:固定窗口,滑动窗口,令牌桶和漏斗。滑动窗口的意思是:维护一个长度固定的窗口,动态统计窗口内请求次数,如果窗口内请求次数超过阈值则不允许访问。

3 实现

参考https://www.jianshu.com/p/cb11e552505b。采用Redis的zset数据结构,将当前请求的时间戳作为score字段,统计窗口时间内请求次数是否超过限制。
完整代码在https://gitcode.com/zsss1/ratelimit/overview

// 限流类型
public enum LimitType {DEFAULT,IP
}
// 限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {String key() default "rate:limiter:";long limit() default 1;long expire() default 1;String message() default "访问频繁";LimitType limitType() default LimitType.IP;
}
// 限流切面
@Component
@Aspect
public class RateLimiterHandler {private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterHandler.class);@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowired@Qualifier("sliding_window")private RedisScript<Long> redisScript;// AOP动态代理com.example包下所有@annotation注解的方法@Around("execution(* com.example..*.*(..)) && @annotation(rateLimiter)")public Object around(ProceedingJoinPoint proceedingJoinPoint, RateLimiter rateLimiter) throws Throwable {Object[] args = proceedingJoinPoint.getArgs();long currentTime = Long.parseLong((String) args[0]);MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();Method method = signature.getMethod();StringBuilder limitKey = new StringBuilder(rateLimiter.key());if (rateLimiter.limitType() == LimitType.IP) {limitKey.append("127.0.0.1");}String className = method.getDeclaringClass().getName();String methodName = method.getName();limitKey.append("_").append(className).append("_").append(methodName);long limitCount = rateLimiter.limit();long windowTime = rateLimiter.expire();List<String> keyList = new ArrayList<>();keyList.add(limitKey.toString());Long result = redisTemplate.execute(redisScript, keyList, windowTime, currentTime, limitCount);if (result != null && result != 1) {throw new RuntimeException(rateLimiter.message());}return proceedingJoinPoint.proceed();}
}
lua">-- 如果允许本次请求,返回1;如果不允许本次请求,返回0
--获取KEYlocal key = KEYS[1]--获取ARGV内的参数-- 缓存时间local expire = tonumber(ARGV[1])-- 当前时间local currentMs = tonumber(ARGV[2])-- 最大次数local limit_count = tonumber(ARGV[3])--窗口开始时间local windowStartMs = currentMs - tonumber(expire * 1000)--获取key的次数local current = redis.call('zcount', key, windowStartMs, currentMs)--如果key的次数存在且大于预设值直接返回当前key的次数if current and tonumber(current) >= limit_count thenreturn 0;
end-- 清除所有过期成员redis.call("ZREMRANGEBYSCORE", key, 0, windowStartMs);-- 添加当前成员redis.call("zadd", key, currentMs, currentMs);redis.call("expire", key, expire);--返回key的次数return 1
// 测试类
// 为了方便统计当前时间,将时间作为请求参数传入接口
@SpringBootTest(classes = DemoApplication.class)
@AutoConfigureMockMvc
public class RateLimitControllerTest {@Autowiredprivate WebApplicationContext webApplicationContext;private MockMvc mockMvc;@BeforeEachpublic void setUp() throws Exception {mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();}@Testpublic void test_rate_limit() throws Exception {String url = "/rate/test";Map<Long, Integer> timeStatusMap = new LinkedHashMap<>();for (int i = 0; i < 20; i++) {Thread.sleep(800);long currentTime = System.currentTimeMillis();MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(url).param("currentTime", String.valueOf(currentTime)).accept(MediaType.APPLICATION_JSON);int status = mockMvc.perform(builder).andReturn().getResponse().getStatus();timeStatusMap.put(currentTime, status);}for (Map.Entry<Long, Integer> entry : timeStatusMap.entrySet()) {Long currentTime = entry.getKey();int status = entry.getValue();int spectedStatus = getStatusOfCurrentTime(currentTime, timeStatusMap.entrySet());System.out.println(status + ", " + spectedStatus + ", " + currentTime);// assertEquals(status, spectedStatus);}}private int getStatusOfCurrentTime(Long currentTime, Set<Map.Entry<Long, Integer>> set) {long startTime = currentTime - 5000;int count = 0;for (Map.Entry<Long, Integer> entry : set) {if (entry.getKey() >= startTime && entry.getKey() < currentTime && entry.getValue() == 200) {count++;}}if (count < 5) {return 200;}return 400;}
}
// 接口
@RestController
@RequestMapping("/rate")
public class RateLimitController {@GetMapping("/test")@RateLimiter(limit = 5, expire = 5, limitType = LimitType.IP)public String test(String currentTime) {return "h";}
}

http://www.ppmy.cn/server/145482.html

相关文章

基于Redis内核的热key统计实现方案|得物技术

一、Redis热key介绍 Redis热key问题是指单位时间内&#xff0c;某个特定key的访问量特别高&#xff0c;占用大量的CPU资源&#xff0c;影响其他请求并导致整体性能降低。而且&#xff0c;如果访问热key的命令是时间复杂度较高的命令&#xff0c;会使得CPU消耗变得更加严重&…

非标自动化项目管理如何做

非标自动化项目管理的关键在于&#xff1a;深入理解客户需求、制定详细的项目计划、有效的资源调配、严格的质量控制、持续的风险管理、高效的沟通协调、灵活应对变更、项目总结与持续改进。深入理解客户需求是项目成功的基础。通过与客户的深入沟通&#xff0c;全面了解其生产…

《进程隔离机制:C++多进程编程安全的坚固堡垒》

在当今数字化时代&#xff0c;软件系统的安全性愈发成为人们关注的焦点。尤其是在 C多进程编程领域&#xff0c;如何确保进程间的安全交互与数据保护&#xff0c;是每一位开发者都必须面对的重要课题。而进程隔离机制&#xff0c;犹如一座坚固的堡垒&#xff0c;为 C多进程编程…

【山大909算法题】2014-T1

文章目录 1.原题2.算法思想3.关键代码4.完整代码5.运行结果 1.原题 为带表头的单链表类Chain编写一个成员函数Reverse&#xff0c;该函数对链表进行逆序操作&#xff08;将链表中的结点按与原序相反的顺序连接&#xff09;&#xff0c;要求逆序操作就地进行&#xff0c;不分配…

阿里云服务器搭建网站CPU、磁盘读取BPS异常问题排查记录

异常现象 阿里云ECS服务器&#xff0c;配置是2C2G&#xff0c;在上面搭建的博客&#xff0c;最近初出现了两次CPU占用异常、磁盘读取BPS异常的问题&#xff0c;并且ssh无法登录。 第一次怀疑是受到了攻击&#xff0c;立马进行了重启、重置登录密码&#xff0c;重新登录进去以后…

React中事件处理和合成事件:理解与使用

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

从 HTML 到 CSS:开启网页样式之旅(二)—— 深入探索 CSS 选择器的奥秘

从 HTML 到 CSS&#xff1a;开启网页样式之旅&#xff08;二&#xff09;—— 深入探索 CSS 选择器的奥秘 前言一、CSS基本选择器1. 通配选择器2. 元素选择器3. 类选择器4. id选择器5.基本选择器总结 二、CSS复合选择器1. 后代选择器2. 子选择器3. 相邻兄弟选择器4.交集选择器5…

高级java每日一道面试题-2024年11月21日-数据结构篇-红黑树有哪几个特征?

如果有遗漏,评论区告诉我进行补充 面试官: 红黑树有哪几个特征? 我回答: 红黑树&#xff08;Red-Black Tree&#xff09;是一种自平衡二叉查找树&#xff08;Self-Balancing Binary Search Tree&#xff09;&#xff0c;它在插入和删除操作后能够自动保持树的高度平衡。红黑…