下面通过Aop自定义注解、Redis + Lua 脚本的方式实现限流,步骤会比较详细 :::info 引入依赖包 ::: pom文件中添加如下依赖包,比较关键的就是spring-boot-starter-data-redisspring-boot-starter-aop

    1. <dependencies>
    2. <dependency>
    3. <groupId>org.springframework.boot</groupId>
    4. <artifactId>spring-boot-starter-web</artifactId>
    5. </dependency>
    6. <dependency>
    7. <groupId>org.springframework.boot</groupId>
    8. <artifactId>spring-boot-starter-data-redis</artifactId>
    9. </dependency>
    10. <dependency>
    11. <groupId>org.springframework.boot</groupId>
    12. <artifactId>spring-boot-starter-aop</artifactId>
    13. </dependency>
    14. <dependency>
    15. <groupId>com.google.guava</groupId>
    16. <artifactId>guava</artifactId>
    17. <version>21.0</version>
    18. </dependency>
    19. <dependency>
    20. <groupId>org.springframework.boot</groupId>
    21. <artifactId>spring-boot-starter-test</artifactId>
    22. </dependency>
    23. <dependency>
    24. <groupId>org.apache.commons</groupId>
    25. <artifactId>commons-lang3</artifactId>
    26. </dependency>
    27. <dependency>
    28. <groupId>org.springframework.boot</groupId>
    29. <artifactId>spring-boot-starter-test</artifactId>
    30. <scope>test</scope>
    31. <exclusions>
    32. <exclusion>
    33. <groupId>org.junit.vintage</groupId>
    34. <artifactId>junit-vintage-engine</artifactId>
    35. </exclusion>
    36. </exclusions>
    37. </dependency>
    38. </dependencies>

    :::info 配置信息application.properties ::: 在application.properties文件中配置提前搭建好的redis服务地址和端口。

    1. spring.redis.host=127.0.0.1
    2. spring.redis.port=6379

    :::info 自定义RedisTemplate :::

    1. @Configuration
    2. public class RedisLimiterHelper {
    3. @Bean
    4. public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
    5. RedisTemplate<String, Serializable> template = new RedisTemplate<>();
    6. template.setKeySerializer(new StringRedisSerializer());
    7. template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    8. template.setConnectionFactory(redisConnectionFactory);
    9. return template;
    10. }
    11. }

    :::info 限流类型枚举 ::: 枚举类字段能够避免在业务处不小心打错字符串,代码更加的规范整洁

    1. /**
    2. * @author liuyanntes
    3. * @description 限流类型
    4. * @date 2020/4/8 13:47
    5. */
    6. public enum LimitType {
    7. /**
    8. * 自定义key
    9. */
    10. CUSTOMER,
    11. /**
    12. * 请求者IP
    13. */
    14. IP;
    15. }

    :::info 自定义AOP注解 ::: 我们自定义个@Limit注解
    注解类型为ElementType.METHOD即作用于方法上

    1. /**
    2. * @author liuyanntes
    3. * @description 自定义限流注解
    4. * @date 2020/4/8 13:15
    5. */
    6. @Target({ElementType.METHOD, ElementType.TYPE})
    7. @Retention(RetentionPolicy.RUNTIME)
    8. @Inherited
    9. @Documented
    10. public @interface Limit {
    11. /**
    12. * 名字
    13. */
    14. String name() default "";
    15. /**
    16. * key
    17. */
    18. String key() default "";
    19. /**
    20. * Key的前缀
    21. */
    22. String prefix() default "";
    23. /**
    24. * 请求限制时间段 单位(秒)
    25. */
    26. int period();
    27. /**
    28. * 在period()内最多访问次数
    29. */
    30. int count();
    31. /**
    32. * 限流的类型,可以根据请求的IP、自定义key
    33. */
    34. LimitType limitType()
    35. }

    :::info AOP切面逻辑 :::

    1. /**
    2. * @author liuyanntes
    3. * @description 限流切面实现
    4. * @date 2020/4/8 13:04
    5. */
    6. @Aspect
    7. @Configuration
    8. public class LimitInterceptor {
    9. private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);
    10. private static final String UNKNOWN = "unknown";
    11. private final RedisTemplate<String, Serializable> limitRedisTemplate;
    12. @Autowired
    13. public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
    14. this.limitRedisTemplate = limitRedisTemplate;
    15. }
    16. /**
    17. * @param pjp
    18. * @author liuyanntes
    19. * @description 切面
    20. * @date 2020/4/8 13:04
    21. */
    22. @Around("execution(public * *(..)) && @annotation(com.xiaofu.limit.api.Limit)")
    23. public Object interceptor(ProceedingJoinPoint pjp) {
    24. MethodSignature signature = (MethodSignature) pjp.getSignature();
    25. Method method = signature.getMethod();
    26. Limit limitAnnotation = method.getAnnotation(Limit.class);
    27. LimitType limitType = limitAnnotation.limitType();
    28. String name = limitAnnotation.name();
    29. String key;
    30. int limitPeriod = limitAnnotation.period();
    31. int limitCount = limitAnnotation.count();
    32. /**
    33. * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key
    34. */
    35. switch (limitType) {
    36. case LimitType.IP:
    37. key = getIpAddress();
    38. break;
    39. case LimitType.CUSTOMER:
    40. key = limitAnnotation.key();
    41. break;
    42. default:
    43. key = StringUtils.upperCase(method.getName());
    44. }
    45. ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
    46. try {
    47. String luaScript = buildLuaScript();
    48. RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
    49. Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
    50. logger.info("Access try count is {} for name={} and key = {}", count, name, key);
    51. if (count != null && count.intValue() <= limitCount) {
    52. return pjp.proceed();
    53. } else {
    54. throw new RuntimeException("You have been dragged into the blacklist");
    55. }
    56. } catch (Throwable e) {
    57. if (e instanceof RuntimeException) {
    58. throw new RuntimeException(e.getLocalizedMessage());
    59. }
    60. throw new RuntimeException("server exception");
    61. }
    62. }
    63. /**
    64. * @author liuyanntes
    65. * @description 编写 redis Lua 限流脚本
    66. * @date 2020/4/8 13:24
    67. */
    68. public String buildLuaScript() {
    69. StringBuilder lua = new StringBuilder();
    70. lua.append("local c");
    71. lua.append("\nc = redis.call('get',KEYS[1])");
    72. // 调用不超过最大值,则直接返回
    73. lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
    74. lua.append("\nreturn c;");
    75. lua.append("\nend");
    76. // 执行计算器自加
    77. lua.append("\nc = redis.call('incr',KEYS[1])");
    78. lua.append("\nif tonumber(c) == 1 then");
    79. // 从第一次调用开始限流,设置对应键值的过期
    80. lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
    81. lua.append("\nend");
    82. lua.append("\nreturn c;");
    83. return lua.toString();
    84. }
    85. /**
    86. * @author liuyanntes
    87. * @description 获取id地址
    88. * @date 2020/4/8 13:24
    89. */
    90. public String getIpAddress() {
    91. HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    92. String ip = request.getHeader("x-forwarded-for");
    93. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
    94. ip = request.getHeader("Proxy-Client-IP");
    95. }
    96. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
    97. ip = request.getHeader("WL-Proxy-Client-IP");
    98. }
    99. if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
    100. ip = request.getRemoteAddr();
    101. }
    102. return ip;
    103. }
    104. }

    :::info 控制层实现 ::: 我们将@Limit注解作用在需要进行限流的接口方法上,在10秒内只允许放行3个请求,这里为直观一点用AtomicInteger计数。

    1. /**
    2. * @Author: liuyanntes
    3. * @Description:
    4. */
    5. @RestController
    6. public class LimiterController {
    7. private static final AtomicInteger ATOMIC_INTEGER_1 = new AtomicInteger();
    8. private static final AtomicInteger ATOMIC_INTEGER_2 = new AtomicInteger();
    9. private static final AtomicInteger ATOMIC_INTEGER_3 = new AtomicInteger();
    10. /**
    11. * @author liuyanntes
    12. * @description
    13. * @date 2020/4/8 13:42
    14. */
    15. @Limit(key = "limitTest", period = 10, count = 3)
    16. @GetMapping("/limitTest1")
    17. public int testLimiter1() {
    18. return ATOMIC_INTEGER_1.incrementAndGet();
    19. }
    20. /**
    21. * @author liuyanntes
    22. * @description
    23. * @date 2020/4/8 13:42
    24. */
    25. @Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER)
    26. @GetMapping("/limitTest2")
    27. public int testLimiter2() {
    28. return ATOMIC_INTEGER_2.incrementAndGet();
    29. }
    30. /**
    31. * @author liuyanntes
    32. * @description
    33. * @date 2020/4/8 13:42
    34. */
    35. @Limit(key = "ip_limit_test", period = 10, count = 3, limitType = LimitType.IP)
    36. @GetMapping("/limitTest3")
    37. public int testLimiter3() {
    38. return ATOMIC_INTEGER_3.incrementAndGet();
    39. }
    40. }

    :::info 限流测试 ::: 测试「预期」:10秒内的连续3次请求均可以成功,第4次请求被拒绝。接下来看一下是不是我们预期的效果
    请求地址:http://127.0.0.1:8080/limitTest1
    Lua限流器实现 - 图1
    可以看到第四次请求时,应用直接拒绝了请求,说明我们基于Aop自定义注解、Redis + Lua的限流方案搭建成功。Lua限流器实现 - 图2