springboot框架项目实践应用十(拦截器+redis实现接口防刷)

1.引言

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

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

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

  • 服务之间调用,不管是ribbon+restTemplate,还是feign都支持超时设置
  • 流控、熔断降级可选择的开源组件有: hystrix、resilience4j、sentinel

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

那么,如果我们是一个创业型的小团队,产品线还没有那么丰富,一些临时性活动支撑。

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

如何实现接口防刷呢?前面我们提到方案肯定是来不及!有没有合适的方案?答案是有:spring拦截器+redis方案。下面来看具体实现。

2.环境准备

2.1.pom.xml

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

xml 复制代码
 <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>

2.2.application.yml

yaml 复制代码
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

2.3.redis相关

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

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

RedisConfig

java 复制代码
/**
 * Redis配置类
 *
 * @author ThinkPad
 * @version 1.0
 */
@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

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

java 复制代码
/**
 * Redis工具类
 *
 * @author ThinkPad
 * @version 1.0
 */
@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);
    }
    
    .........................省略其它代码....................
}

2.4.拦截器相关

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

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

WebConfig

java 复制代码
/**
 * web 配置
 *
 * @author ThinkPad
 * @version 1.0
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LimitInterceptor limitInterceptor;

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加限流拦截器
        registry.addInterceptor(limitInterceptor);

    }
}

AccessLimit

java 复制代码
/**
 * 自定义注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {

 // 过期时间 单位:秒
 int seconds();

 // 最大请求次数
 int maxCount();

}

LimitInterceptor

java 复制代码
/**
 * 限流 interceptor
 *
 * @author ThinkPad
 * @version 1.0
 */
@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();

  }

}

2.5.应用controller

java 复制代码
/**
 * 限流controller
 *
 * @author ThinkPad
 * @version 1.0
 */
@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.";
    }
}

3.案例效果

启动应用,分别访问端点

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

本文源码:gitee.com/yanghouhua/...,子模块: follow-me-springboot-interceptor

相关推荐
JavaPub-rodert43 分钟前
golang 的 goroutine 和 channel
开发语言·后端·golang
ivygeek2 小时前
MCP:基于 Spring AI Mcp 实现 webmvc/webflux sse Mcp Server
spring boot·后端·mcp
Hi-Jimmy2 小时前
【VolView】纯前端实现CT三维重建-CBCT
前端·架构·volview·cbct
GoGeekBaird3 小时前
69天探索操作系统-第54天:嵌入式操作系统内核设计 - 最小内核实现
后端·操作系统
鱼樱前端3 小时前
Java Jdbc相关知识点汇总
java·后端
流烟默4 小时前
编写脚本在Linux下启动、停止SpringBoot工程
linux·spring boot·shell
canonical_entropy4 小时前
NopReport示例-动态Sheet和动态列
java·后端·excel
南山十一少4 小时前
Spring Boot + Spring Integration整合MQTT打造双向通信客户端
spring boot·物联网
kkk哥4 小时前
基于springboot的母婴商城系统(018)
java·spring boot·后端
小咕聊编程4 小时前
【含文档+PPT+源码】基于SpringBoot+Vue旅游管理网站
vue.js·spring boot·旅游