接口限流是项目中常见的需求,也就是为了限制项目中的某一个接口在一段时间内进行频繁访问,导致系统压力增加。
本文主要介绍 redis+lua 进行接口限流。
demo 地址:https://github.com/SoftLeaderGy/StartRedis/tree/master/redis-boot/src/main/java/com/yang/redisboot/currentlimit

# 搭建

# 创建一个自定义限流注解

  • 代码
import java.lang.annotation.*;
/**
 * @Description: 自定义竹节实现分布式限流
 * @Author: Guo.Yang
 * @Date: 2023/09/22/10:47
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLimitStream {
    /**
     * 请求限制,一秒内可以允许好多个进入 (默认一秒可以支持 100 个)
     * @return
     */
    int reqLimit() default 100;
    /**
     * 模块名称
     * @return
     */
    String reqName() default "";
}
  • 参数说明
    • reqLimit 限流个数,一秒能进来的请求个数,默认 100 个
    • reqName 模块名称

# 创建一个测试 Controller

  • 代码
import com.yang.redisboot.currentlimit.annotation.RedisLimitStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @Description:
 * @Author: Guo.Yang
 * @Date: 2023/09/22/10:49
 */
@RestController
@RequestMapping("/limit")
public class LimitTestController {
    /**
     * 压测接口,测试接口限流
     * @return
     */
    @GetMapping("/test")
    @RedisLimitStream(reqName = "测试接口限流", reqLimit = 5)
    public String limitTest(){
        return "success";
    }
}

# 创建一个限流的 lua 脚本

  • limit.lua
local key = KEYS[1] -- 限流 KEY(一秒一个)
local limit = tonumber(ARGV[1]) -- 限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then -- 如果超出限流大小
    return false
else -- 请求数 + 1,并设置 2 秒过期
    redis.call("INCRBY", key, "1")
    redis.call("expire", key, "2")
end
return true

将 lua 脚本放在项目中的 resources 目录下

  • 说明
    • KEYS[1] 用来表示在 redis 中用作键值的参数占位,主要用來传递在 redis 中用作 keyz 值的参数。
    • ARGV[1] 用来表示在 redis 中用作参数的占位,主要用来传递在 redis 中用做 value 值的参数。
    • INCRBY redis 操作,将 key 以指定数量进行增加
    • expire redis 操作,将 key 设置过期时间

# 创建一个配置类,在启动的时候将我们的 lua 脚本代码加载到 redisscript 中

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
/**
 * @Description: 将 lua 脚本加载到 RedisScript 中
 * @Author: Guo.Yang
 * @Date: 2023/09/22/10:49
 */
@Configuration
public class RedisConfiguration {
    /**
     * 初始化将 lua 脚本加载到 redis 脚本中
     * @return
     */
    @Bean
    public DefaultRedisScript loadRedisScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript();
        redisScript.setLocation(new ClassPathResource("limit.lua"));
        redisScript.setResultType(Boolean.class);
        return redisScript;
    }
}

# 创建限流 Aop,拦截相关包下的接口

import com.yang.redisboot.currentlimit.annotation.RedisLimitStream;
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
/**
 * @Description: MyRedisLimiter 注解的切面类
 * @Author: Guo.Yang
 * @Date: 2023/09/22/10:49
 */
@Aspect
@Component
@Slf4j
public class RedisLimiterAspect {
    /**
     * 当前响应请求
     */
    @Autowired
    private HttpServletResponse response;
    /**
     * redis 服务
     */
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    /**
     * 执行 redis 的脚本文件
     */
    @Autowired(required = false)
    private RedisScript<Boolean> rateLimitLua;
    /**
     * 对所有接口进行拦截
     */
    @Pointcut("execution(public * com.yang.redisboot.currentlimit.controller.*.*(..))")
    public void pointcut(){}
    /**
     * 对切点进行继续处理
     */
    @Around("pointcut()")
    public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        // 使用反射获取 RedisLimitStream 注解
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        // 没有添加限流注解的方法直接放行
        RedisLimitStream redisLimitStream = signature.getMethod().getDeclaredAnnotation(RedisLimitStream.class);
        // 如果接口没有限流注解,直接放行即可
        if(ObjectUtils.isEmpty(redisLimitStream)){
            return proceedingJoinPoint.proceed();
        }
        // 创建一个 list,将 key 放入 list,为之后 redis 执行 lua 脚本做所需要 key 的形式
        List<String> keyList = new ArrayList<>();
        keyList.add("ip:" + (System.currentTimeMillis() / 1000));
        // 获取接口上限流注解设置的限流次数
        int value = redisLimitStream.reqLimit();
        //redis 执行 lua 脚本
        /**
         * 参数说明:
         *  1、lua 脚本,通过 RedisConfiguration 中的 DefaultRedisScript 配置加载进来
         *  2、list 形式的 redis key
         *  3、redis value
         */
        boolean acquired = (Boolean) redisTemplate.execute(rateLimitLua, keyList, value);
        log.info("执行lua结果:" + acquired);
        // 通过执行结果判断接口是否已经到了接口限制次数
        if(!acquired){
            // 执行接口限流返回
            this.limitStreamBackMsg();
            return null;
        }
        // 获取到令牌,继续向下执行
        return proceedingJoinPoint.proceed();
    }
    /**
     * 被拦截的人,提示消息
     */
    private void limitStreamBackMsg() {
        log.info("当前排队人较多,请稍后再试!");
        response.setHeader("Content-Type", "text/html;charset=UTF8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.println("{\"code\":503,\"message\":\"当前排队人较多,请稍后再试!\",\"data\":\"null\"}");
            writer.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }
}

到此为止限流方案就算是搭建完毕了,目录结构如下
image.png

# 测试

  • 测试工具 api post
  • 为了方便测试,接口限流注解设置成一秒限制为 5 个流量

image.png

  • 压力测试,1 秒 6 个流量

image.png

  • 测试结果

image.png
image.png
5 个成功,1 个失败,限流成功。

更新于

请我喝[茶]~( ̄▽ ̄)~*

GuoYang 微信支付

微信支付

GuoYang 支付宝

支付宝