一、概述
服务器流量控制一直都是一个非常重要的问题。因为服务器是有性能瓶颈的,所以后台的接口也有其性能瓶颈,当辛辛苦苦的把多级缓存做好后,觉得可以承受高并发了的时候,服务突然就蹦了,可能是缓存爆掉了,也可能是数据库宕机了。造成这些问题的大多数原因就是流量太高了的问题。当然我们也可以进行服务的分布式部署。但今天不讨论这个,就讨论单体应用。
单体应用的限流算法还是比较多的。
1.AtomicInteger自己手动实现
2.漏铜算法
3.令牌算法
今天就介绍下令牌算法,而且使用Guava框架自带的。
原理如下:
系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理, 则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。 令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。
二、示例(两种方法实现,其一是原生,其二是使用注解帮我们简化)
1.引入guava和apo
<!--本地缓存for guava cache--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>24.0-jre</version> <!-- 面向切面编程--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
2.使用原生实现
@Slf4j @RestController @RequestMapping("/api/v1/pub/limit/") public class GuavaCurrentLimitingController { /** * 限流策略:1秒钟 2个请求(这里表示这个接口1秒钟服务器最多接收两个并发,其他请求直接返回人说过多的提示。) */ private final RateLimiter limiter = RateLimiter.create(2.0); private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @GetMapping("/test") public void test() { //500毫秒内没拿到令牌就进行服务降级 boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MICROSECONDS); //如果没拿到令牌就进行服务降级 if (!tryAcquire) { log.info("当前排队的人数过多,请稍后再试{}", LocalDateTime.now().format(dtf)); } log.info("请求成功{}", LocalDateTime.now().format(dtf)); } }
上述代码可以解决单体应用的简单限流问题。但是代码写的不够优雅。
下面借助AOP+注解的形式对代码进行优化
1.定义个Limit注解
/** * Guava自定义限流注解 */ @Retention(RetentionPolicy.RUNTIME)//运行时注解 @Target({ElementType.METHOD})//注解用在方法上 @Documented public @interface Limit { /** * 唯一id * 作用:不同的接口执行不同的限流策略 */ String id() default ""; /** * 最多访问次数限制 */ double permitsPerSecond(); /** * 获取令牌最大等待时间 */ long timeout(); /** * 获取令牌最大等待时间单位(默认毫秒) */ TimeUnit timeunit() default TimeUnit.MILLISECONDS; /** * 无法获取令牌的提示语 */ String errMsg() default "当前请求人数过多,请稍后再试"; }
2.定义一个AOP类,来解析这个注解
/** * 使用自定义guava限流注解 * * @author Tony * @version 2023 * @date 2023/9/27 10:56 */ @Slf4j @Aspect @Component public class GuavaLimit { /** * 存储不同接口不同限流策略的map */ private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); @Around("@annotation(com.tony.cursor.limit.Limit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿limit的注解 Limit limit = method.getAnnotation(Limit.class); if (limit != null) { //id作用:不同的接口,不同的流量控制 String id = limit.id(); RateLimiter rateLimiter = null; //验证缓存是否有命中key if (!limitMap.containsKey(id)) { // 创建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSecond()); limitMap.put(id, rateLimiter); log.info("新建了令牌桶={},容量={}", id, limit.permitsPerSecond()); } rateLimiter = limitMap.get(id); // 拿令牌 boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit()); // 拿不到命令,直接返回异常提示 if (!acquire) { log.info("令牌桶={},获取令牌失败", id); throw new CustomException(limit.errMsg());//如果没有拿到令牌就直接抛异常 } } return joinPoint.proceed(); } }
3.使用注解进行限流
@Slf4j @RestController @RequestMapping("/api/v1/pub/limit/") public class GuavaCurrentLimitingController { @GetMapping("/test2") @Limit(id = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS, errMsg = "当前排队人数过多,请稍后再试") public String test2() { log.info("拿到令牌请求成功"); return "已拿到令牌,请求成功"; } }
好了,优化完毕,测试结果如下所示:
标签:令牌,单体,public,限流,limit,注解,Guava,id From: https://www.cnblogs.com/tony-yang-flutter/p/17732343.html