Aop限流实现解决方案
01、限流
在业务场景中,为了限制某些业务的并发,造成接口的压力,需要增加限流功能。
02、限流的成熟解决方案
- guava (漏斗算法 + 令牌算法) (单机限流)
- redis + lua + ip 限流(比较推荐)(分布式限流)
- nginx 限流 (源头限流)
03、 限流的目的
- 保护服务的资源泄露
- 解决服务器的高可压,减少服务器并发
04、安装redis服务
(1)安装redis
wget http://download.redis.io/releases/redis-6.0.6.tar.gz
tar xzf redis-6.0.6.tar.gz
cd redis-6.0.6
make
(2)修改redis.conf
daemonize yes
# bind 127.0.0.1
protected-mode no
requirepass 123456
(3)如果你之前启动过redis服务器,请麻烦一定要先检查,把服务杀掉,在启动
ps -ef | grep redis
kill redispid
(4)然后重启服务,一定指定配置文件启动
./src/redis-server ./redis.conf
05、创建springboot项目整合redis

(1)导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.qbb.limit</groupId>
<artifactId>redis-lua-limit</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-lua-limit</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
(2)修改配置文件
server:
port:9001
spring:
redis:
host: 192.168.137.72
port: 6379
database: 0
lettuce:
pool:
max-active: 20
max-wait: -1
max-idle: 5
min-idle: 0
application:
name: redis-lua-limit
(3)创建一个Redis配置类
说明一下:为什么要创建一个redis配置类,直接用SpringBoot自动装配的RedisTemplate不行么?
主要原因是:springboot本身在RedisAutoConfiguration里面已经初始化好了RedisTemplate。但是这个RedisTemplate序列化key的时候是以Object的类型进行序列化,所以看到 "\xac\xed\x00\x05t\x00\x14age11111111111111111" 字符串不友好。所以就写一个配置类进行覆盖了。
package com.qbb.limit.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
* @version 1.0
* @date 2022-05-16 23:34
* @Description:
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1: 开始创建一个redistemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 2:开始redis连接工厂跪安了
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 创建一个json的序列化方式
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置key用string序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置value用jackjson进行处理
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hash也要进行修改
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 默认调用
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
(4)定义限流lua脚本(这个可以使用我下面提供的,也可以在网上直接百度,如果感兴趣也可以自己研究研究)
在resources目录下的lua文件夹下,新建一个iplimite.lua文件,文件内容如下:
-- 为某个接口的请求IP设置计数器,比如:127.0.0.1请求接口
-- KEYS[1] = 127.0.0.1 也就是用户的IP
-- ARGV[1] = 过期时间 30m
-- ARGV[2] = 限制的次数
local limitCount = redis.call('incr',KEYS[1]);
if limitCount == 1 then
redis.call("expire",KEYS[1],ARGV[2])
end
-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口
if limitCount > tonumber(ARGV[1]) then
return false
end
return true
(5)在config包中创建一个LuaConfig的Lua限流脚本配置类
lua配置类主要是去加载lua文件的内容,放到内存中。方便redis去读取和控制。
package com.qbb.limit.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
/**
* @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
* @version 1.0
* @date 2022-05-16 23:45
* @Description:
*/
@Configuration
public class LuaConfig {
/**
* 将lua脚本的内容加载出来放入到DefaultRedisScript
*
* @return
*/
@Bean
public DefaultRedisScript<Boolean> ipLimitLua() {
DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/iplimite.lua")));
defaultRedisScript.setResultType(Boolean.class);
return defaultRedisScript;
}
}
(6)自定义一个限流注解(为什么要用注解,两个字:方便)

package com.qbb.limit.aop;
import java.lang.annotation.*;
/**
* @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
* @version 1.0
* @date 2022-05-16 23:57
* @Description:
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {
// 每timeout限制请求的个数
int limit() default 10;
// 时间,单位默认是秒
int timeout() default 1;
}
(7)创建一个获取用户访问IP的工具类(网上百度的)
package com.qbb.limit.utils;
import javax.servlet.http.HttpServletRequest;
/**
* @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
* @version 1.0
* @date 2022-05-17 0:01
* @Description:
*/
public class RequestUtils {
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}
(8)定义核心限流AOP切面类
package com.qbb.limit.core;
import com.google.common.collect.Lists;
import com.qbb.limit.aop.AccessLimiter;
import com.qbb.limit.utils.RequestUtils;
import lombok.extern.slf4j.Slf4j;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
* @version 1.0
* @date 2022-05-17 0:06
* @Description:
*/
@Component
@Aspect
@Slf4j
public class LimiterAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private DefaultRedisScript<Boolean> ipLimiterLuaScript;
@Autowired
private DefaultRedisScript<Boolean> ipLimitLua;
// 1: 切入点
@Pointcut("@annotation(com.qbb.limit.aop.AccessLimiter)")
public void limiterPointcut() {
}
@Before("limiterPointcut()")
public void limiter(JoinPoint joinPoint) {
log.info("限流进来了.......");
// 1:获取方法的签名作为key
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String classname = methodSignature.getMethod().getDeclaringClass().getName();
String packageName = methodSignature.getMethod().getDeclaringClass().getPackage().getName();
log.info("classname:{},packageName:{}", classname, packageName);
// 4: 读取方法的注解信息获取限流参数
AccessLimiter annotation = method.getAnnotation(AccessLimiter.class);
// 5:获取注解方法名
String methodNameKey = method.getName();
// 6:获取服务请求的对象
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
HttpServletResponse response = requestAttributes.getResponse();
String userIp = RequestUtils.getIpAddr(request);
log.info("用户IP是:.......{}", userIp);
// 7:通过方法反射获取注解的参数
Integer limit = annotation.limit();
Integer timeout = annotation.timeout();
String redisKey = method + ":" + userIp;
// 8: 请求lua脚本
Boolean acquired = stringRedisTemplate.execute(ipLimitLua, Lists.newArrayList(redisKey), limit.toString(), timeout.toString());
// 如果超过限流限制
if (!acquired) {
// 抛出异常,然后让全局异常去处理
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter writer = response.getWriter();) {
// 解决报错:getWriter() has already been called for this response] with root cause
writer.print("<h1>客官你慢点,请稍后在试一试!!!</h1>");
writer.flush();
} catch (Exception ex) {
throw new RuntimeException("客官你慢点,请稍后在试一试!!!");
}
}
}
}
(9)编写测试代码
package com.qbb.limit.controller;
import com.qbb.limit.aop.AccessLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
* @version 1.0
* @date 2022-05-17 0:16
* @Description:
*/
@RestController
public class HelloController {
@GetMapping("/hello")
@AccessLimiter(timeout = 1, limit = 3) // 1秒钟超过3次限流
public String index() {
// 分布锁
return "success";
}
@GetMapping("/hello2")
public String index2() {
return "success";
}
}
访问一次没问题
快速刷新试试
Aop限流实现解决方案的更多相关文章
- 基于kubernetes的分布式限流
做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机 ...
- Redisson多策略注解限流
限流:使用Redisson的RRateLimiter进行限流 多策略:map+函数式接口优化if判断 自定义注解 /** * aop限流注解 */ @Target({ElementType.METHO ...
- 库存秒杀问题-redis解决方案- 接口限流
<?php/** * Created by PhpStorm. * redis 销量超卖秒杀解决方案 * redis 文档:http://doc.redisfans.com/ * ab -n 1 ...
- 基于AOP和Redis实现对接口调用情况的监控及IP限流
目录 需求描述 概要设计 代码实现 参考资料 需求描述 项目中有许多接口,现在我们需要实现一个功能对接口调用情况进行统计,主要功能如下: 需求一:实现对每个接口,每天的调用次数做记录: 需求二:如果某 ...
- 使用AOP和Semaphore对项目中具体的某一个接口进行限流
整体思路: 一 具体接口,可以自定义一个注解,配置限流量,然后对需要限流的方法加上注解即可! 二 容器初始化的时候扫描所有所有controller,并找出需要限流的接口方法,获取对应的限流量 三 使用 ...
- coding++:高并发解决方案限流技术-使用RateLimiter实现令牌桶限流-Demo
RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率. 通常可应用于抢购限流防止冲垮系统:限制某接口.服务单位时 ...
- springboot + aop + Lua分布式限流的最佳实践
整理了一些Java方面的架构.面试资料(微服务.集群.分布式.中间件等),有需要的小伙伴可以关注公众号[程序员内点事],无套路自行领取 一.什么是限流?为什么要限流? 不知道大家有没有做过帝都的地铁, ...
- Spring Cloud限流思路及解决方案
转自: http://blog.csdn.net/zl1zl2zl3/article/details/78683855 在高并发的应用中,限流往往是一个绕不开的话题.本文详细探讨在Spring Clo ...
- 基于redis+lua实现高并发场景下的秒杀限流解决方案
转自:https://blog.csdn.net/zzaric/article/details/80641786 应用场景如下: 公司内有多个业务系统,由于业务系统内有向用户发送消息的服务,所以通过统 ...
- coding++:高并发解决方案限流技术--计数器--demo
1.它是限流算法中最简单最容易的一种算法 计数器实现限流 每分钟只允许10个请求 第一个请求进去的时间为startTime,在startTime + 60s内只允许10个请求 当60s内超过十个请求后 ...
随机推荐
- ATtiny88初体验(六):SPI
ATtiny88初体验(六):SPI SPI介绍 ATtiny88自带SPI模块,可以实现数据的全双工三线同步传输.它支持主从两种模式,可以配置为LSB或者MSB优先传输,有7种可编程速率,支持从空闲 ...
- JAVA-Springboot实践项目-用户注册
Smiling & Weeping ----我本没喜欢的人, 见你的次数多了, 也就有了. 1.创建数据表 1.1.选中数据表: use store 1.2.创建t_user表: 2创建用户实 ...
- Unity 游戏开发、01 基础篇 | 阿发入门篇全课程学习笔记
Unity Documentation .全课程视频 .第15,24章视频 afanihao Unity入门,全课程内容个人学习笔记,简单部分一笔带过,重点内容带 2.3 窗口布局 Unity默认窗口 ...
- 为不断增长的Go生态系统扩展gopls
原文在这里. 由 Robert Findley and Alan Donovan 发布于 2023年9月8日 今年夏天初,Go团队发布了gopls的v0.12版本,这是Go语言的语言服务器,它进行了核 ...
- 开源社区赋能,Walrus 用户体验再升级
基于平台工程理念的应用管理平台 Walrus 已于上月正式开源,目前在 GitHub 已收获 177 颗星 Walrus 希望打造简洁清爽的应用部署与管理体验,帮助研发与运维团队减少"内耗& ...
- KRPANO资源分析工具下载720YUN全景图
提示:目前分析工具中的全景图下载功能将被极速全景图下载大师替代,相比分析工具,极速全景图下载大师支持更多的网站(包括各类KRPano全景网站,和百度街景) 详细可以查看如下的链接: 极速全景图下载大师 ...
- MD5&MD5盐值加密到BCryptPasswordEncoder
MD5&MD5盐值加密 Message Digest algorithm5,信息摘要算法: 压缩性:任意长度的数据,算出的MD5值长度都是固定的 容易计算:从原数据计算出MD5值很容易 抗修改 ...
- 【RocketMQ】【源码】延迟消息实现原理
RocketMQ设定了延迟级别可以让消息延迟消费,延迟消息会使用SCHEDULE_TOPIC_XXXX这个主题,每个延迟等级对应一个消息队列,并且与普通消息一样,会保存每个消息队列的消费进度(dela ...
- http、socket以及websocket的区别(websocket使用举例)
一.http.socket.websocket介绍 1.HTTP(Hypertext Transfer Protocol):HTTP是一种应用层协议,用于在客户端和服务器之间传输超文本数据.它是基于请 ...
- Ds100p -「数据结构百题」61~70
61.P5355 [Ynoi2017]由乃的玉米田 由乃在自己的农田边散步,她突然发现田里的一排玉米非常的不美. 这排玉米一共有 \(N\) 株,它们的高度参差不齐. 由乃认为玉米田不美,所以她决定出 ...


