接口限流是项目中常见的需求,也就是为了限制项目中的某一个接口在一段时间内进行频繁访问,导致系统压力增加。
本文主要介绍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(); | |
} | |
} | |
} | |
} |
到此为止限流方案就算是搭建完毕了,目录结构如下
# 测试
- 测试工具 api post
- 为了方便测试,接口限流注解设置成一秒限制为 5 个流量
- 压力测试,1 秒 6 个流量
- 测试结果
5 个成功,1 个失败,限流成功。