架构系列九(服务容错一个接口防刷简易实现方案)

149 阅读6分钟

接口防刷这个事情,其实就是限流。在我们探讨服务容错的方案中,通常有以下可选择的方案

  • 超时:我调用你,你迟迟不给响应,超过一定时间我就不等你了,免得受到你的影响
  • 流控(限流):你们怎么都一起来找我,我一次只服务3个人,超过3个的我不管
  • 熔断降级:资源不够用,不可用了,为了保障核心链路可用,其它的都让一让

那么针对以上服务容错方案,业界可选择的产品也是比较丰富的,比如说

  • 服务之间调用,比如说http客户端相关工具,不管是ribbon+restTemplate,还是feign都支持超时设置
  • 流控、熔断降级可选择的开源组件有: hystrix、resilience4j、sentinel

这些方案组件从功能、特性都比较丰富强大,要用好,需要团队中有专门的小伙伴去研究吃透,简单来说就是使用成本相对比较高,适合在平台级产品线中去使用。

如果我们是一个创业型的小团队,产品线还没有那么丰富,自然杀鸡就用不上牛刀了。

比如说,中秋节了,领导决定做一个客户、用户、员工线上答谢活动,即临时组织一个线上秒杀活动,那么这个任务,自然就交给了技术组的小伙伴。

既然是一个秒杀活动,并发、接口防刷的问题是一定要考虑的,关键是公司内部目前还没有成熟的产品可以支撑,需要临时开发一个,而且时间还比较紧张!过两天就要上线了!

如何实现接口防刷呢?前面我们提到方案肯定是来不及了,那么还有什么可行的方案吗?答案是有的

  • 你应该记得redis的过期特性,正好可以满足我们实现限流的问题(比如说1分钟内,只允许5个请求),实现一个有过期时间的计数器就可以了
  • 该秒杀活动,是一个临时活动,只限于中秋节使用,过了就不再需要关注了,那么我们搭建一个简单的单体应用就够了

经过以上讨论分析,技术组的小伙伴选择了springboot作为基础框架,并通过redis解决接口防刷的问题,下面我们一起来模拟实现,通过本文章的分享,期望给小伙伴们带来以下收获

  • springboot整合redis应用
  • 自定义注解应用
  • springboot中,拦截器的使用

我们开始吧!

1.案例环境准备

1.1.pom.xml

springboot整合redis使用,需要导入spring-boot-starter-data-redis依赖

 <dependencies>
     <!--web mvc依赖-->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
​
     <!--redis 依赖-->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>
​
     <!--fast json依赖-->
     <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>fastjson</artifactId>
         <version>1.2.43</version>
     </dependency>
​
     <!--lombok依赖-->
     <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
     </dependency>
​
     <!--test 依赖-->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
     </dependency>
</dependencies>

1.2.application.yml

server:
  port: 8080
spring:
  application:
    name: follow-me-springboot-interceptor
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    jedis:
      pool:
        min-idle: 8
        max-idle: 8
        max-active: 100
        max-wait: 100000ms
    timeout: 5000ms

1.3.redis相关

为了方便使用,编写一个redis配置类,以及一个redis工具类

  • RedisConfig:用于初始配置RedisTemplate模板工具,指定key/value相关的序列化实现
  • RedisUtil:redis工具类,在RedisTemplate模板的基础上,封装redis相关操作,更加方便业务使用

RedisConfig

/**
 * Redis配置类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/9/12 8:13
 */
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
​
    /**
     * redisTemplate
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
​
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
​
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
​
        // value值的序列化采用fastJsonRedisSerializer
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);
​
        // 设置连接工厂
        template.setConnectionFactory(redisConnectionFactory);
​
        return template;
    }
​
    /**
     * stringRedisTemplate
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
​
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
​
        return template;
    }
​
}

RedisUtil

整个工具类,代码量比较大,需要完整看的小伙伴请到代码仓库看,我已经把整个案例代码上传到代码仓库

/**
 * Redis工具类
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/9/12 8:13
 */
@Component
public final class RedisUtil {
​
    /**
     * 注入redisTemplate
     */
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    
    .........................省略其它代码....................
     /**
     * 获取缓存
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
​
    /**
     * 存入缓存
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    
    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long inCr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
    
    .........................省略其它代码....................
}

1.4.拦截器相关

编写一个拦截器,以及拦截器的配置

  • WebConfig:该配置类实现WebMvcConfigurer,用于web mvc相关的配置,本案例中用于配置拦截器
  • AccessLimit:注解,用于标注需要访问限制的接口
  • LimitInterceptor:访问限制处理拦截器

WebConfig

/**
 * web 配置
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/9/12 8:31
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
​
    @Autowired
    private LimitInterceptor limitInterceptor;
​
    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加限流拦截器
        registry.addInterceptor(limitInterceptor);
​
    }
}

AccessLimit

/**
 * 自定义注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
​
 // 过期时间 单位:秒
 int seconds();
​
 // 最大请求次数
 int maxCount();
​
}

LimitInterceptor

/**
 * 限流 interceptor
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/9/12 8:31
 */
@Component
@Slf4j
public class LimitInterceptor extends HandlerInterceptorAdapter {
​
 @Autowired
 private RedisUtil redisUtil;
​
 /**
  * 前置处理
  * @param request
  * @param response
  * @param handler
  * @return
  * @throws Exception
  */
  @Override
  public boolean preHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler) throws Exception {
      // 当前请求url
      String url = request.getRequestURI();
​
      // 判断请求是否属于方法的请求
      if(handler instanceof HandlerMethod){
          HandlerMethod hm = (HandlerMethod) handler;
​
          // 检查注解AccessLimit,若没有注解,直接放行
          AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
          if(accessLimit == null){
              log.info("当前正在请求接口:{},该接口不需要限制访问.", url);
              return true;
          }
​
          // 过期时间,最大请求次数
          int seconds = accessLimit.seconds();
          int maxCount = accessLimit.maxCount();
          log.info("当前正在请求接口:{},该接口有访问限制需求,时间:{},最大访问次数:{}",
                  url, seconds, maxCount);
​
          // 限流key
          String key = "rate:limit" + url;
          key = key.replaceAll("/", ":");
          Integer count = 0;
​
          // 从redis中获取用户访问的次数
          Object keyValue = redisUtil.get(key);
          if(keyValue != null){
              count = (Integer) keyValue;
          }
​
          // 第1次访问
          if(count == 0){
              redisUtil.set(key, 1, seconds);
          }else if(count < maxCount){
              // 第2到maxCount-1次访问
              redisUtil.inCr(key,1);
          }else{
              // 大于等于maxCount次访问
              String content = String.format("您正在访问的接口:%s,超出了访问限制阈值:%d",url, maxCount);
              render(response, content);
              return false;
​
          }
​
​
      }
​
    return true;
​
  }
​
  /**
   * 封装返回值
   * @param response
   * @param msg
   * @throws Exception
   */
  private void render(HttpServletResponse response, String msg)throws Exception {
      response.setContentType("application/json;charset=UTF-8");
      OutputStream out = response.getOutputStream();
      out.write(msg.getBytes("UTF-8"));
      out.flush();
​
      out.close();
​
  }
​
}

1.5应用controller

/**
 * 限流controller
 *
 * @author ThinkPad
 * @version 1.0
 * @date 2021/9/12 8:31
 */
@RestController
public class RateLimitController {
​
    @Autowired
    private RedisUtil redisUtil;
​
    /**
     * 测试方法
     * @return
     */
    @RequestMapping("noLimit")
    public String noLimit(){
        // 测试redis工具
        redisUtil.inCr("rate:limit:test", 1L);
        return "no limit.";
    }
​
    /**
     * 需要限流方法
     * @return
     */
    @RequestMapping("needLimit")
    @AccessLimit(seconds=60, maxCount=5)
    public String rateLimit(){
​
        return "need limit.";
    }
}

2.案例效果

启动应用,分别访问端点

客户端连接redis,观察计数器

image.png