前言

在开发分布式高并发系统时有三把利器用来保护系统:缓存、降级、限流。

缓存

缓存的目的是提升系统访问速度和增大系统处理容量

降级

降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开

限流

限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
本文主要讲的是api接口限流相关内容,虽然不是论述高并发概念中的限流, 不过道理都差不多。通过限流可以让系统维持在一个相对稳定的状态,为更多的客户提供服务。
API 接口的限流主要应用场景有:
1、 电商系统(特别是6.18、双11等)中的秒杀活动,使用限流防止使用软件恶意刷单;
2、 各种基础api接口限流:例如天气信息获取,IP对应城市接口,百度、腾讯等对外提供的基础接口,都是通过限流来实现免费与付费直接的转换。
3、 被各种系统广泛调用的api接口,严重消耗网络、内存等资源,需要合理限流。

api限流实战

一、SpringBoot中集成Redis

SpringBoot中集成Redis相对比较简单,步骤如下:
1、 1引入Redis依赖;

  1. <!--springboot redis依赖-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis</artifactId>
  5. </dependency>

1、 2在application.yml中配置Redis;

  1. spring:
  2. redis:
  3. database: 3 # Redis数据库索引(默认为0)
  4. host: 127.0.0.1 # Redis服务器地址
  5. port: 6379 # Redis服务器连接端口
  6. password: 123456 # Redis服务器连接密码(默认为空)
  7. timeout: 2000 # 连接超时时间(毫秒)
  8. jedis:
  9. pool:
  10. max-active: 200 # 连接池最大连接数(使用负值表示没有限制)
  11. max-idle: 20 # 连接池中的最大空闲连接
  12. min-idle: 0 # 连接池中的最小空闲连接
  13. max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)

1、 3配置RedisTemplate;

  1. /**
  2. * @Description: redis配置类
  3. * @Author oyc
  4. */
  5. @Configuration
  6. @EnableCaching
  7. public class RedisConfig extends CachingConfigurerSupport {
  8. /**
  9. * RedisTemplate相关配置
  10. * 使redis支持插入对象
  11. *
  12. * @param factory
  13. * @return 方法缓存 Methods the cache
  14. */
  15. @Bean
  16. public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
  17. RedisTemplate<String, Object> template = new RedisTemplate<>();
  18. // 配置连接工厂
  19. template.setConnectionFactory(factory);
  20. // 设置key的序列化器
  21. template.setKeySerializer(new StringRedisSerializer());
  22. // 设置value的序列化器
  23. //使用Jackson 2,将对象序列化为JSON
  24. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  25. //json转对象类,不设置默认的会将json转成hashmap
  26. ObjectMapper om = new ObjectMapper();
  27. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  28. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  29. jackson2JsonRedisSerializer.setObjectMapper(om);
  30. template.setValueSerializer(jackson2JsonRedisSerializer);
  31. return template;
  32. }
  33. }

以上,已经完成Redis的集成,后续使用可以直接注入RedisTemplate,如下所示:

  1. @Autowired
  2. private RedisTemplate<String, Object> redisTemplate;

二、实现限流

2、 1添加自定义AccessLimit注解;
使用注解方式实现接口的限流操作,方便而优雅。

  1. /**
  2. * @Description:
  3. * @Author oyc
  4. */
  5. @Inherited
  6. @Documented
  7. @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. public @interface AccessLimit {
  10. /**
  11. * 指定second 时间内 API请求次数
  12. */
  13. int maxCount() default 5;
  14. /**
  15. * 请求次数的指定时间范围 秒数(redis数据过期时间)
  16. */
  17. int second() default 60;
  18. }

2、 2编写拦截器;
限流的思路
1、 通过路径:ip的作为key,访问次数为value的方式对某一用户的某一请求进行唯一标识
2、 每次访问的时候判断key是否存在,是否count超过了限制的访问次数
3、 若访问超出限制,则应response返回msg:请求过于频繁给前端予以展示

  1. /**
  2. * @Description: 访问拦截器
  3. * @Author oyc
  4. */
  5. @Component
  6. public class AccessLimitInterceptor implements HandlerInterceptor {
  7. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  8. @Autowired
  9. private RedisTemplate<String, Object> redisTemplate;
  10. @Override
  11. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  12. try {// Handler 是否为 HandlerMethod 实例
  13. if (handler instanceof HandlerMethod) {
  14. // 强转
  15. HandlerMethod handlerMethod = (HandlerMethod) handler;
  16. // 获取方法
  17. Method method = handlerMethod.getMethod();
  18. // 是否有AccessLimit注解
  19. if (!method.isAnnotationPresent(AccessLimit.class)) {
  20. return true;
  21. }
  22. // 获取注解内容信息
  23. AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
  24. if (accessLimit == null) {
  25. return true;
  26. }
  27. int seconds = accessLimit.second();
  28. int maxCount = accessLimit.maxCount();
  29. // 存储key
  30. String key = request.getRemoteAddr() + ":" + request.getContextPath() + ":" + request.getServletPath();
  31. // 已经访问的次数
  32. Integer count = (Integer) redisTemplate.opsForValue().get(key);
  33. System.out.println("已经访问的次数:" + count);
  34. if (null == count || -1 == count) {
  35. redisTemplate.opsForValue().set(key, 1, seconds, TimeUnit.SECONDS);
  36. return true;
  37. }
  38. if (count < maxCount) {
  39. redisTemplate.opsForValue().increment(key);
  40. return true;
  41. }
  42. if (count >= maxCount) {
  43. logger.warn("请求过于频繁请稍后再试");
  44. return false;
  45. }
  46. }
  47. return true;
  48. } catch (Exception e) {
  49. logger.warn("请求过于频繁请稍后再试");
  50. e.printStackTrace();
  51. }
  52. return true;
  53. }
  54. }

2、 3注册拦截器并配置拦截路径和不拦截路径;

  1. /**
  2. * @Description: 访问拦截器配置
  3. * @Author oyc
  4. */
  5. @Configuration
  6. public class IntercepterConfig implements WebMvcConfigurer {
  7. @Autowired
  8. private AccessLimitInterceptor accessLimitInterceptor;
  9. @Override
  10. public void addInterceptors(InterceptorRegistry registry) {
  11. registry.addInterceptor(accessLimitInterceptor)
  12. .addPathPatterns("/**").excludePathPatterns("/static/**","/login.html","/user/login");
  13. }
  14. }

2、 4使用AccessLimit;

  1. /**
  2. * @Description:
  3. * @Author oyc
  4. */
  5. @RestController
  6. @RequestMapping("access")
  7. public class AccessLimitController {
  8. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  9. /**
  10. * 限流测试
  11. */
  12. @GetMapping
  13. @AccessLimit(maxCount = 3,second = 60)
  14. public String limit(HttpServletRequest request) {
  15. logger.error("Access Limit Test");
  16. return "限流测试";
  17. }
  18. }

2、 5测试;
8f4863fa97b688c30de6fb02c9e69b51.png
源码传送门:

https://github.com/oycyqr/springboot-learning-demo/tree/master/springboot-validated