Aop限流实现解决方案
2022-05-17 00:52
621 查看
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"; } }
访问一次没问题
快速刷新试试
相关文章推荐
- (转)Spring事务处理时自我调用的解决方案及一些实现方式的风险
- Android实现推送方式解决方案----感觉很有用
- BootstrapTable+KnockoutJS相结合实现增删改查解决方案(三)两个Viewmodel搞定增删改查
- springboot(ssm)+spring security+druid+layui+xadmin2.2实现简单权限管理系统之问题总结和解决方案
- jquery ajax() 404错误,406错误解决方案 遍历json数组 append到指定位置 ajax实现点击加载更多按钮
- Android实现推送方式解决方案
- Android实现推送方式解决方案
- 解决方案实现在UITableView中显示数据
- Android实现推送方式解决方案
- Android实现推送方式解决方案
- 分布式事务解决方案及实现
- Redis实现高并发下的抢购,秒杀,解决方案
- 基于HTML5实现的在线3D虚拟试衣系统(试衣间)解决方案
- CAS构建和实现单点登录解决方案
- Java微信公众平台开发(九)——关键字回复以及客服接口实现(该公众号暂时无法提供服务解决方案)
- asp.net中TextBox里面实现回车触发按钮button的解决方案
- Android实现推送方式解决方案
- 【go语言实现服务器接收http请求以及出现泄漏时的解决方案】
- Android实现推送方式解决方案
- 千万级并发实现的秘密:内核不是解决方案,而是问题所在