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 8.9 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. # spring-boot-demo-ratelimit-redis
  2. > 此 demo 主要演示了 Spring Boot 项目如何通过 AOP 结合 Redis + Lua 脚本实现分布式限流,旨在保护 API 被恶意频繁访问的问题,是 `spring-boot-demo-ratelimit-guava` 的升级版。
  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-redis</artifactId>
  11. <version>1.0.0-SNAPSHOT</version>
  12. <packaging>jar</packaging>
  13. <name>spring-boot-demo-ratelimit-redis</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>org.springframework.boot</groupId>
  36. <artifactId>spring-boot-starter-data-redis</artifactId>
  37. </dependency>
  38. <!-- 对象池,使用redis时必须引入 -->
  39. <dependency>
  40. <groupId>org.apache.commons</groupId>
  41. <artifactId>commons-pool2</artifactId>
  42. </dependency>
  43. <dependency>
  44. <groupId>cn.hutool</groupId>
  45. <artifactId>hutool-all</artifactId>
  46. </dependency>
  47. <dependency>
  48. <groupId>org.springframework.boot</groupId>
  49. <artifactId>spring-boot-starter-test</artifactId>
  50. <scope>test</scope>
  51. </dependency>
  52. <dependency>
  53. <groupId>org.projectlombok</groupId>
  54. <artifactId>lombok</artifactId>
  55. <optional>true</optional>
  56. </dependency>
  57. </dependencies>
  58. <build>
  59. <finalName>spring-boot-demo-ratelimit-redis</finalName>
  60. <plugins>
  61. <plugin>
  62. <groupId>org.springframework.boot</groupId>
  63. <artifactId>spring-boot-maven-plugin</artifactId>
  64. </plugin>
  65. </plugins>
  66. </build>
  67. </project>
  68. ```
  69. ### 1.2. 限流注解
  70. ```java
  71. /**
  72. * <p>
  73. * 限流注解,添加了 {@link AliasFor} 必须通过 {@link AnnotationUtils} 获取,才会生效
  74. * </p>
  75. *
  76. * @author yangkai.shen
  77. * @date Created in 2019-09-30 10:31
  78. * @see AnnotationUtils
  79. */
  80. @Target(ElementType.METHOD)
  81. @Retention(RetentionPolicy.RUNTIME)
  82. @Documented
  83. public @interface RateLimiter {
  84. long DEFAULT_REQUEST = 10;
  85. /**
  86. * max 最大请求数
  87. */
  88. @AliasFor("max") long value() default DEFAULT_REQUEST;
  89. /**
  90. * max 最大请求数
  91. */
  92. @AliasFor("value") long max() default DEFAULT_REQUEST;
  93. /**
  94. * 限流key
  95. */
  96. String key() default "";
  97. /**
  98. * 超时时长,默认1分钟
  99. */
  100. long timeout() default 1;
  101. /**
  102. * 超时时间单位,默认 分钟
  103. */
  104. TimeUnit timeUnit() default TimeUnit.MINUTES;
  105. }
  106. ```
  107. ### 1.3. AOP处理限流
  108. ```java
  109. /**
  110. * <p>
  111. * 限流切面
  112. * </p>
  113. *
  114. * @author yangkai.shen
  115. * @date Created in 2019-09-30 10:30
  116. */
  117. @Slf4j
  118. @Aspect
  119. @Component
  120. @RequiredArgsConstructor(onConstructor_ = @Autowired)
  121. public class RateLimiterAspect {
  122. private final static String SEPARATOR = ":";
  123. private final static String REDIS_LIMIT_KEY_PREFIX = "limit:";
  124. private final StringRedisTemplate stringRedisTemplate;
  125. private final RedisScript<Long> limitRedisScript;
  126. @Pointcut("@annotation(com.xkcoding.ratelimit.redis.annotation.RateLimiter)")
  127. public void rateLimit() {
  128. }
  129. @Around("rateLimit()")
  130. public Object pointcut(ProceedingJoinPoint point) throws Throwable {
  131. MethodSignature signature = (MethodSignature) point.getSignature();
  132. Method method = signature.getMethod();
  133. // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解
  134. RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
  135. if (rateLimiter != null) {
  136. String key = rateLimiter.key();
  137. // 默认用类名+方法名做限流的 key 前缀
  138. if (StrUtil.isBlank(key)) {
  139. key = method.getDeclaringClass().getName()+StrUtil.DOT+method.getName();
  140. }
  141. // 最终限流的 key 为 前缀 + IP地址
  142. // TODO: 此时需要考虑局域网多用户访问的情况,因此 key 后续需要加上方法参数更加合理
  143. key = key + SEPARATOR + IpUtil.getIpAddr();
  144. long max = rateLimiter.max();
  145. long timeout = rateLimiter.timeout();
  146. TimeUnit timeUnit = rateLimiter.timeUnit();
  147. boolean limited = shouldLimited(key, max, timeout, timeUnit);
  148. if (limited) {
  149. throw new RuntimeException("手速太快了,慢点儿吧~");
  150. }
  151. }
  152. return point.proceed();
  153. }
  154. private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) {
  155. // 最终的 key 格式为:
  156. // limit:自定义key:IP
  157. // limit:类名.方法名:IP
  158. key = REDIS_LIMIT_KEY_PREFIX + key;
  159. // 统一使用单位毫秒
  160. long ttl = timeUnit.toMillis(timeout);
  161. // 当前时间毫秒数
  162. long now = Instant.now().toEpochMilli();
  163. long expired = now - ttl;
  164. // 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String
  165. Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + "");
  166. if (executeTimes != null) {
  167. if (executeTimes == 0) {
  168. log.error("【{}】在单位时间 {} 毫秒内已达到访问上限,当前接口上限 {}", key, ttl, max);
  169. return true;
  170. } else {
  171. log.info("【{}】在单位时间 {} 毫秒内访问 {} 次", key, ttl, executeTimes);
  172. return false;
  173. }
  174. }
  175. return false;
  176. }
  177. }
  178. ```
  179. ### 1.4. lua 脚本
  180. ```lua
  181. -- 下标从 1 开始
  182. local key = KEYS[1]
  183. local now = tonumber(ARGV[1])
  184. local ttl = tonumber(ARGV[2])
  185. local expired = tonumber(ARGV[3])
  186. -- 最大访问量
  187. local max = tonumber(ARGV[4])
  188. -- 清除过期的数据
  189. -- 移除指定分数区间内的所有元素,expired 即已经过期的 score
  190. -- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired
  191. redis.call('zremrangebyscore', key, 0, expired)
  192. -- 获取 zset 中的当前元素个数
  193. local current = tonumber(redis.call('zcard', key))
  194. local next = current + 1
  195. if next > max then
  196. -- 达到限流大小 返回 0
  197. return 0;
  198. else
  199. -- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score]
  200. redis.call("zadd", key, now, now)
  201. -- 每次访问均重新设置 zset 的过期时间,单位毫秒
  202. redis.call("pexpire", key, ttl)
  203. return next
  204. end
  205. ```
  206. ### 1.5. 接口测试
  207. ```java
  208. /**
  209. * <p>
  210. * 测试
  211. * </p>
  212. *
  213. * @author yangkai.shen
  214. * @date Created in 2019-09-30 10:30
  215. */
  216. @Slf4j
  217. @RestController
  218. public class TestController {
  219. @RateLimiter(value = 5)
  220. @GetMapping("/test1")
  221. public Dict test1() {
  222. log.info("【test1】被执行了。。。。。");
  223. return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~");
  224. }
  225. @GetMapping("/test2")
  226. public Dict test2() {
  227. log.info("【test2】被执行了。。。。。");
  228. return Dict.create().set("msg", "hello,world!").set("description", "我一直都在,卟离卟弃");
  229. }
  230. @RateLimiter(value = 2, key = "测试自定义key")
  231. @GetMapping("/test3")
  232. public Dict test3() {
  233. log.info("【test3】被执行了。。。。。");
  234. return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~");
  235. }
  236. }
  237. ```
  238. ### 1.6. 其余代码参见 demo
  239. ## 2. 测试
  240. - 触发限流时控制台打印
  241. ![image-20190930155856711](http://static.xkcoding.com/spring-boot-demo/ratelimit/redis/063812.jpg)
  242. - 触发限流的时候 Redis 的数据
  243. ![image-20190930155735300](http://static.xkcoding.com/spring-boot-demo/ratelimit/redis/063813.jpg)
  244. ## 3. 参考
  245. - [mica-plus-redis 的分布式限流实现](https://github.com/lets-mica/mica/tree/master/mica-plus-redis)
  246. - [Java并发:分布式应用限流 Redis + Lua 实践](https://segmentfault.com/a/1190000016042927)