优雅的接口防刷处理方式,看这一篇就够了

发布于:2022-12-10 ⋅ 阅读:(955) ⋅ 点赞:(0)

1、API接口防刷

1.1、概念

顾名思义,就是要实现某个接口在某段时间内只能让某人访问指定次数,超出次数,就不让访问了

1.2、原理

  • 在请求的时候,服务器通过 Redis 记录下你请求的次数,如果次数超过限制就不给访问
  • 在 Redis 保存的 Redis 是有时效性的,过期就会删除

1.3、目的

主要防止短时间接口被大量调用(攻击),出现系统崩溃和系统爬虫问题,提升服务的可用性

1.4、实现方案介绍

  1. 拦截器+自定义注解+Redis
  2. AOP+自定义注解+Redis

我这里准备了一份基础代码:https://gitee.com/colinWu_java/spring-boot-base.git
接下来我会在此主干代码基础上进行开发

2、方案一

好,接下来我们直接实战编码,运用到项目中的话,非常实用,使用起来也非常方便,下面是我们需要写的几个核心类

  1. 自定义注解
  2. 拦截器(核心)
  3. Redis配置类(设置序列化用)

2.1、自定义注解

package org.wujiangbo.annotation;

import java.lang.annotation.*;

/**
 * 用于防刷限流的注解
 *      默认是5秒内只能调用一次
 */
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    
    /** 限流的key */
    String key() default "limit:";

    /** 周期,单位是秒 */
    int cycle() default 5;

    /** 请求次数 */
    int count() default 1;

    /** 默认提示信息 */
    String msg() default "请勿重复点击";
}

2.2、拦截器

package org.wujiangbo.interceptor;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.wujiangbo.annotation.RateLimit;
import org.wujiangbo.exception.MyException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

/**
 * 防刷限流的拦截器
 * @author wujiangbo
 * @date 2022-08-23 18:39
 */
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws Exception {
        // 如果请求的是方法,则需要做校验
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 获取目标方法上是否有指定注解
            RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
            if (rateLimit == null) {
                //说明目标方法上没有 RateLimit 注解
                return true;
            }
            //代码执行到此,说明目标方法上有 RateLimit 注解,所以需要校验这个请求是不是在刷接口
            // 获取请求IP地址
            String ip = getIpAddr(request);
            // 请求url路径
            String uri = request.getRequestURI();
            //存到redis中的key
            String key = "RateLimit:" + ip + ":" + uri;
            // 缓存中存在key,在限定访问周期内已经调用过当前接口
            if (redisTemplate.hasKey(key)) {
                // 访问次数自增1
                redisTemplate.opsForValue().increment(key, 1);
                // 超出访问次数限制
                if (redisTemplate.opsForValue().get(key) > rateLimit.count()) {
                    throw new MyException(rateLimit.msg());
                }
                // 未超出访问次数限制,不进行任何操作,返回true
            } else {
                // 第一次设置数据,过期时间为注解确定的访问周期
                redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(), TimeUnit.SECONDS);
            }
            return true;
        }
        //如果请求的不是方法,直接放行
        return true;
    }
    
    //获取请求的归属IP地址
    private String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }
}

2.3、Redis配置类

package org.wujiangbo.config.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        // 设置序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        RedisSerializer<?> stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);// key序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
        redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

2.4、注册拦截器

package cn.wujiangbo.config;

import cn.wujiangbo.interceptor.RateLimitInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * 配置拦截器
 * @author wujiangbo
 * @date 2022-08-23 18:51
 */
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

    @Autowired
    private RateLimitInterceptor rateLimitInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor);
    }
}

项目需要导入依赖:

<!--Redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

yml配置文件如下:

server:
  port: 8001
  undertow:
    # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
    # 不要设置过大,如果过大,启动项目会报错:打开文件数过多(CPU有几核,就填写几)
    io-threads: 6
    # 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
    # 它的值设置取决于系统线程执行任务的阻塞系数,默认值是:io-threads * 8
    worker-threads: 48
    # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
    # 每块buffer的空间大小,越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可
    buffer-size: 1024
    # 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region
    buffers-per-region: 1024
    # 是否分配的直接内存(NIO直接分配的堆外内存)
    direct-buffers: true
spring:
  #redis配置
  redis:
    # 数据库索引
    database: 0
    # 地址
    host: 127.0.0.1
    # 端口,默认为6379
    port: 6379
    # 密码
    password: 123456
    # 连接超时时间
    timeout: 30s
    jedis:
      pool:
        time-between-eviction-runs: 1000
        max-active: 200
        max-wait: -1ms
        min-idle: 5
        max-idle: 20
  #配置数据库链接信息
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test1?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&rewriteBatchedStatements=true
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  application:
    name: springboot #服务名

#MyBatis-Plus相关配置
mybatis-plus:
  #指定Mapper.xml路径,如果与Mapper路径相同的话,可省略
  mapper-locations: classpath:org/wujiangbo/mapper/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true #开启驼峰大小写自动转换
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启控制台sql输出

2.5、测试

package org.wujiangbo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wujiangbo.annotation.CheckPermission;
import org.wujiangbo.annotation.RateLimit;
import org.wujiangbo.result.JSONResult;

/**
 * @desc 测试接口类
 * @author 波波老师(weixin:javabobo0513)
 */
@RestController
@Slf4j
public class TestController {

    //5秒内只能访问2次
    @RateLimit(key= "testLimit", count = 2, cycle = 6, msg = "同志,不要请求这么快,好吗")
    @GetMapping("/test001")
    public JSONResult rate() {
        System.out.println("成功发送一条短信");
        return JSONResult.success();
    }
}

打开浏览器访问:http://localhost:8081/test/test001

开始返回结果:

在这里插入图片描述

但是当你多刷几次后,就显示报错信息了:

在这里插入图片描述

很显然,我们接口防刷功能就实现了,测试成功

以上代码已经全部提交到:https://gitee.com/colinWu_java/spring-boot-base.git
在分支【InterfacePreventAttack】中

2.6、Lua脚本实现方案

针对上面方案我们可以改用lua脚本实现,关于lua介绍:

  • lua本身就是一种编程语言,是一个小巧的脚本语言
  • 性能非常高

我们在Redis的场景中使用lua脚本有以下好处:

  1. 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输
  2. 原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务
  3. 复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用

这里我们的需求是做限流,思路是根据用户的IP和访问的URI来进行计数,达到一定数量之后进行限制访问。这应该是限流操作的计算法,另外还有令牌算法和漏桶算法

我们这里介绍最简单的计算法,首先我们在项目的resources目录下新建limit.lua文件,里面内容如下:

local key = "rate.limit:" .. KEYS[1] --限流KEY
local limit = tonumber(ARGV[1])      --限流大小
local cycle = ARGV[2]                --过期周期
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
   return 0
else  --请求数+1,并设置过期时间
   redis.call("INCRBY", key, "1")
   redis.call("expire", key, cycle)
   return current + 1
end

上面就是我们的lua限流脚本

然后我们Redis配置类中新增下面方法,用来读取上面的lua脚本:

@Bean
public DefaultRedisScript<Number> redisluaScript() {
    DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
    //读取 lua 脚本
    redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
    redisScript.setResultType(Number.class);
    return redisScript;
}

然后拦截器类代码改成下面这样了:

package cn.wujiangbo.interceptor;

import cn.wujiangbo.annotation.RateLimit;
import cn.wujiangbo.exception.MyException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;

/**
 * 防刷限流的拦截器
 * @author wujiangbo
 * @date 2022-08-23 18:39
 */
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    @Autowired
    private DefaultRedisScript<Number> redisLuaScript;

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws Exception {
        // 如果请求的是方法,则需要做校验
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 获取目标方法上是否有指定注解
            RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
            if (rateLimit == null) {
                //说明目标方法上没有 RateLimit 注解
                return true;
            }
            //代码执行到此,说明目标方法上有 RateLimit 注解,所以需要校验这个请求是不是在刷接口
            // 获取请求IP地址
            String ip = getIpAddr(request);
            // 请求url路径
            String uri = request.getRequestURI();
            //存到redis中的key
            String key = rateLimit.key() + ip + ":" + uri;

            //将key转成List类型
            List<String> keys = Collections.singletonList(key);

            Number number = redisTemplate.execute(redisLuaScript, keys, rateLimit.count(), rateLimit.cycle());

            if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
                System.out.println(rateLimit.cycle() + "秒内访问第:" + number.toString() + " 次" + getCurrentTime());
                return true;
            }
            throw new MyException(rateLimit.msg());
        }
        //如果请求的不是方法,直接放行
        return true;
    }

    //获取当前时间
    public static String getCurrentTime(){
        LocalDateTime localDateTime = LocalDateTime.now();
        return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }

    //获取请求的归属IP地址
    private String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }
}

测试代码:

package cn.wujiangbo.controller;

import cn.wujiangbo.annotation.RateLimit;
import cn.wujiangbo.result.JSONResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试接口
 * @author wujiangbo
 * @date 2022-08-23 18:50
 */
@RestController
@RequestMapping("/test")
public class TestController {

    //4秒内只能访问2次
    @RateLimit(key= "testLimit", count = 2, cycle = 4, msg = "同志,不要请求这么快,好吗")
    @GetMapping("/test001")
    public JSONResult rate() {
        System.out.println("成功发送一条短信");
        return JSONResult.success();
    }
}

然后浏览器再访问做测试,就可以实现4秒内只能访问两次接口了

3、方案二

AOP方案需要导入依赖:

<!-- aop切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3.1、自定义注解

package cn.wujiangbo.annotation;

import java.lang.annotation.*;

/**
 * 用于防刷限流的注解
 *      默认是5秒内只能调用一次
 * @author wujiangbo
 * @date 2022-08-23 18:36
 */
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

    /** 限流的key */
    String key() default "limit:";

    /** 周期,单位是秒 */
    int cycle() default 5;

    /** 请求次数 */
    int count() default 1;

    /** 默认提示信息 */
    String msg() default "请勿重复点击";
}

3.2、切面类

package cn.wujiangbo.aspect;

import cn.wujiangbo.annotation.RateLimit;
import cn.wujiangbo.exception.MyException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * 切面类:实现限流校验
 * @author wujiangbo
 * @date 2022-08-24 11:27
 */
@Aspect
@Component
public class AccessLimitAspect {

    @Resource
    private RedisTemplate<String, Integer> redisTemplate;

    /**
     * 这里我们使用注解的形式
     * 当然,我们也可以通过切点表达式直接指定需要拦截的package,需要拦截的class 以及 method
     */
    @Pointcut("@annotation(cn.wujiangbo.annotation.RateLimit)")
    public void limitPointCut() {
    }

    /**
     * 环绕通知
     */
    @Around("limitPointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // 获取被注解的方法
        MethodInvocationProceedingJoinPoint mjp = (MethodInvocationProceedingJoinPoint) pjp;
        MethodSignature signature = (MethodSignature) mjp.getSignature();
        Method method = signature.getMethod();

        // 获取方法上的注解
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);
        if (rateLimit == null) {
            // 如果没有注解,则继续调用,不做任何处理
            return pjp.proceed();
        }
        /**
         * 代码走到这里,说明有 RateLimit 注解,那么就需要做限流校验了
         *  1、这里可以使用Redis的API做计数校验
         *  2、这里也可以使用Lua脚本做计数校验,都可以
         */
        //获取request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取请求IP地址
        String ip = getIpAddr(request);
        // 请求url路径
        String uri = request.getRequestURI();
        //存到redis中的key
        String key = "RateLimit:" + ip + ":" + uri;
        // 缓存中存在key,在限定访问周期内已经调用过当前接口
        if (redisTemplate.hasKey(key)) {
            // 访问次数自增1
            redisTemplate.opsForValue().increment(key, 1);
            // 超出访问次数限制
            if (redisTemplate.opsForValue().get(key) > rateLimit.count()) {
                throw new MyException(rateLimit.msg());
            }
            // 未超出访问次数限制,不进行任何操作,返回true
        } else {
            // 第一次设置数据,过期时间为注解确定的访问周期
            redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(), TimeUnit.SECONDS);
        }
        return pjp.proceed();
    }

    //获取请求的归属IP地址
    private String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }
}

3.3、测试

package cn.wujiangbo.controller;

import cn.wujiangbo.annotation.RateLimit;
import cn.wujiangbo.result.JSONResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试接口
 * @author wujiangbo
 * @date 2022-08-23 18:50
 */
@RestController
@RequestMapping("/test")
public class TestController {

    //4秒内只能访问2次
    @RateLimit(key= "testLimit", count = 2, cycle = 4, msg = "同志,不要请求这么快,好吗")
    @GetMapping("/test001")
    public JSONResult rate() {
        System.out.println("成功发送一条短信");
        return JSONResult.success();
    }
}

然后浏览器再访问做测试,就可以实现4秒内只能访问两次接口了

4、限流算法介绍(了解)

4.1、令牌桶算法

  • 令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌
  • 桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝
  • 当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不到,该请求就要被限流,要么直接丢弃,要么在缓冲区等待

4.2、漏桶算法

  • 漏桶算法的实现往往依赖于队列,请求到达如果队列未满则直接放入队列,然后有一个处理器按照固定频率从队列头取出请求进行处理
  • 如果请求量大,则会导致队列满,那么新来的请求就会被抛弃

5、总结

  1. 实际项目中,接口防刷是一个非常普遍的需求
  2. 一般的处理方案都是采用自定义注解+拦截器+Redis处理的
本文含有隐藏内容,请 开通VIP 后查看