You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 6.5 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. # spring-boot-demo-ratelimit-guava
  2. > 此 demo 主要演示了 Spring Boot 项目如何通过 AOP 结合 Guava 的 RateLimiter 实现限流,旨在保护 API 被恶意频繁访问的问题。
  3. ## 1. 主要代码
  4. ### 1.1. pom.xml
  5. ```xml
  6. <?xml version="1.0" encoding="UTF-8"?>
  7. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  8. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  9. <modelVersion>4.0.0</modelVersion>
  10. <artifactId>spring-boot-demo-ratelimit-guava</artifactId>
  11. <version>1.0.0-SNAPSHOT</version>
  12. <packaging>jar</packaging>
  13. <name>spring-boot-demo-ratelimit-guava</name>
  14. <description>Demo project for Spring Boot</description>
  15. <parent>
  16. <groupId>com.xkcoding</groupId>
  17. <artifactId>spring-boot-demo</artifactId>
  18. <version>1.0.0-SNAPSHOT</version>
  19. </parent>
  20. <properties>
  21. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  22. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  23. <java.version>1.8</java.version>
  24. </properties>
  25. <dependencies>
  26. <dependency>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-web</artifactId>
  29. </dependency>
  30. <dependency>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-starter-aop</artifactId>
  33. </dependency>
  34. <dependency>
  35. <groupId>cn.hutool</groupId>
  36. <artifactId>hutool-all</artifactId>
  37. </dependency>
  38. <dependency>
  39. <groupId>com.google.guava</groupId>
  40. <artifactId>guava</artifactId>
  41. </dependency>
  42. <dependency>
  43. <groupId>org.springframework.boot</groupId>
  44. <artifactId>spring-boot-starter-test</artifactId>
  45. <scope>test</scope>
  46. </dependency>
  47. <dependency>
  48. <groupId>org.projectlombok</groupId>
  49. <artifactId>lombok</artifactId>
  50. <optional>true</optional>
  51. </dependency>
  52. </dependencies>
  53. <build>
  54. <finalName>spring-boot-demo-ratelimit-guava</finalName>
  55. <plugins>
  56. <plugin>
  57. <groupId>org.springframework.boot</groupId>
  58. <artifactId>spring-boot-maven-plugin</artifactId>
  59. </plugin>
  60. </plugins>
  61. </build>
  62. </project>
  63. ```
  64. ### 1.2. 定义一个限流注解 `RateLimiter.java`
  65. > 注意代码里使用了 `AliasFor` 设置一组属性的别名,所以获取注解的时候,需要通过 `Spring` 提供的注解工具类 `AnnotationUtils` 获取,不可以通过 `AOP` 参数注入的方式获取,否则有些属性的值将会设置不进去。
  66. ```java
  67. /**
  68. * <p>
  69. * 限流注解,添加了 {@link AliasFor} 必须通过 {@link AnnotationUtils} 获取,才会生效
  70. *
  71. * @author yangkai.shen
  72. * @date Created in 2019-09-12 14:14
  73. * @see AnnotationUtils
  74. * </p>
  75. */
  76. @Target(ElementType.METHOD)
  77. @Retention(RetentionPolicy.RUNTIME)
  78. @Documented
  79. public @interface RateLimiter {
  80. int NOT_LIMITED = 0;
  81. /**
  82. * qps
  83. */
  84. @AliasFor("qps") double value() default NOT_LIMITED;
  85. /**
  86. * qps
  87. */
  88. @AliasFor("value") double qps() default NOT_LIMITED;
  89. /**
  90. * 超时时长
  91. */
  92. int timeout() default 0;
  93. /**
  94. * 超时时间单位
  95. */
  96. TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
  97. }
  98. ```
  99. ### 1.3. 定义一个切面 `RateLimiterAspect.java`
  100. ```java
  101. /**
  102. * <p>
  103. * 限流切面
  104. * </p>
  105. *
  106. * @author yangkai.shen
  107. * @date Created in 2019-09-12 14:27
  108. */
  109. @Slf4j
  110. @Aspect
  111. @Component
  112. public class RateLimiterAspect {
  113. private static final ConcurrentMap<String, com.google.common.util.concurrent.RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
  114. @Pointcut("@annotation(com.xkcoding.ratelimit.guava.annotation.RateLimiter)")
  115. public void rateLimit() {
  116. }
  117. @Around("rateLimit()")
  118. public Object pointcut(ProceedingJoinPoint point) throws Throwable {
  119. MethodSignature signature = (MethodSignature) point.getSignature();
  120. Method method = signature.getMethod();
  121. // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解
  122. RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
  123. if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) {
  124. double qps = rateLimiter.qps();
  125. if (RATE_LIMITER_CACHE.get(method.getName()) == null) {
  126. // 初始化 QPS
  127. RATE_LIMITER_CACHE.put(method.getName(), com.google.common.util.concurrent.RateLimiter.create(qps));
  128. }
  129. log.debug("【{}】的QPS设置为: {}", method.getName(), RATE_LIMITER_CACHE.get(method.getName()).getRate());
  130. // 尝试获取令牌
  131. if (RATE_LIMITER_CACHE.get(method.getName()) != null && !RATE_LIMITER_CACHE.get(method.getName()).tryAcquire(rateLimiter.timeout(), rateLimiter.timeUnit())) {
  132. throw new RuntimeException("手速太快了,慢点儿吧~");
  133. }
  134. }
  135. return point.proceed();
  136. }
  137. }
  138. ```
  139. ### 1.4. 定义两个API接口用于测试限流
  140. ```java
  141. /**
  142. * <p>
  143. * 测试
  144. * </p>
  145. *
  146. * @author yangkai.shen
  147. * @date Created in 2019-09-12 14:22
  148. */
  149. @Slf4j
  150. @RestController
  151. public class TestController {
  152. @RateLimiter(value = 1.0, timeout = 300)
  153. @GetMapping("/test1")
  154. public Dict test1() {
  155. log.info("【test1】被执行了。。。。。");
  156. return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~");
  157. }
  158. @GetMapping("/test2")
  159. public Dict test2() {
  160. log.info("【test2】被执行了。。。。。");
  161. return Dict.create().set("msg", "hello,world!").set("description", "我一直都在,卟离卟弃");
  162. }
  163. }
  164. ```
  165. ## 2. 测试
  166. - test1 接口未被限流的时候
  167. <img src="http://static.xkcoding.com/spring-boot-demo/ratelimit/guava/063719.jpg" alt="image-20190912155209716" style="zoom:50%;" />
  168. - test1 接口频繁刷新,触发限流的时候
  169. <img src="http://static.xkcoding.com/spring-boot-demo/ratelimit/guava/063718-1.jpg" alt="image-20190912155229745" style="zoom:50%;" />
  170. - test2 接口不做限流,可以一直刷新
  171. <img src="http://static.xkcoding.com/spring-boot-demo/ratelimit/guava/063718.jpg" alt="image-20190912155146012" style="zoom:50%;" />
  172. ## 3. 参考
  173. - [限流原理解读之guava中的RateLimiter](https://juejin.im/post/5bb48d7b5188255c865e31bc)
  174. - [使用Guava的RateLimiter做限流](https://my.oschina.net/hanchao/blog/1833612)