需求:XX接口访问量太大,需要在一定时间内不让那么多的请求进来
实现原理:
用Redis作为限流组件的核心的原理,将接口名称当Key,一段时间内访问次数为value,同时设置该Key过期时间。
限制 XX接口在TT时间内访问次数
第一次访问 操作redis,key:接口名称 value:次数 expire设置过期时间 TT
第二次访问 操作redis, value + 1,如果过期则按照第一次处理通过lua脚本 来保证原子性
推荐使用Lua脚本。
- 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;
- 原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
- 复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.
Redis添加了对Lua的支持,能够很好的满足原子性、事务性的支持,让我们免去了很多的异常逻辑处理。
源码地址:https://gitee.com/love_yu_0698/ratelimiter-demo.git
实现方式:添加注解,通过AOP切面完成接口限流
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/**
* 限流key
* @return
*/
String key() default "rate:limiter";
/**
* 单位时间限制通过请求数
* @return
*/
long limit() default 3L;
/**
* 过期时间,单位秒
* @return
*/
long expire() default 30L;
/**
* 返回值
* @return
*/
String message() default "false";
}
AOP 处理 限流次数
init() 在应用启动时会初始化DefaultRedisScript,并加载Lua脚本,方便进行调用。
Lua脚本放置在classpath下,通过ClassPathResource进行加载。
获取 @RateLimiter 注解配置的属性:key、limit、expire,并通过 redisTemplate.execute(RedisScript script, List keys, Object... args) 方法传递给Lua脚本进行限流相关操作 脚本返回状态为0则为触发限流 返回message 默认为 false,1表示正常请求。
@Aspect
@Component
@Slf4j
public class RateLimterAspect {
@Resource
private RedisTemplate redisTemplate;
private DefaultRedisScript<Long> getRedisScript;
@PostConstruct
public void init() {
getRedisScript = new DefaultRedisScript<>();
getRedisScript.setResultType(Long.class);
getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimter.lua")));
log.info("RateLimterAspect[分布式限流处理器]脚本加载完成");
}
@Pointcut("@annotation(com.crayon.ratelimiterdemo.annotation.RateLimiter)")
public void rateLimiter() {}
@Around("@annotation(rateLimiter)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, RateLimiter rateLimiter) throws Throwable {
if (log.isDebugEnabled()){
log.debug("RateLimterAspect[分布式限流处理器]开始执行限流操作");
}
Signature signature = proceedingJoinPoint.getSignature();
if (!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("the Annotation @RateLimter must used on method!");
}
// 限流模块key
String limitKey = rateLimiter.key();
Preconditions.checkNotNull(limitKey);
// 限流阈值
long limitTimes = rateLimiter.limit();
// 限流超时时间
long expireTime = rateLimiter.expire();
if (log.isDebugEnabled()){
log.debug("RateLimterAspect[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}", limitTimes, expireTime);
}
// 限流提示语
String message = rateLimiter.message();
if (message == null || "".equalsIgnoreCase(message.replace(" ",""))) {
message = "false";
}
//执行Lua脚本
List<String> keyList = new ArrayList<>();
// 设置key值为注解中的值
keyList.add(limitKey);
//调用脚本并执行
@SuppressWarnings("unchecked")
Long result = (Long) redisTemplate.execute(getRedisScript, keyList, expireTime, limitTimes);
if (result != null && result == 0) {
String msg = "由于超过单位时间=" + expireTime + "-允许的请求次数=" + limitTimes + "[触发限流]";
log.debug(msg);
return message;
}
if (log.isDebugEnabled()){
log.debug("RateLimterAspect[分布式限流处理器]限流执行结果-result={},请求[正常]响应", result);
}
return proceedingJoinPoint.proceed();
}
}
Lua脚本
限流操作的核心,通过执行一个Lua脚本进行限流的操作
- 首先脚本获取Java代码中传递而来的要限流的模块的key,不同的模块key值一定不能相同,否则会覆盖!
- redis.call('incr', key1)对传入的key做incr操作,如果key首次生成,设置超时时间ARGV[1];(初始值为1)
- ttl是为防止某些key在未设置超时时间并长时间已经存在的情况下做的保护的判断;
- 每次请求都会做+1操作,当限流的值val大于我们注解的阈值,则返回0表示已经超过请求限制,触发限流。否则为正常请求。
--获取KEY
local key1 = KEYS[1]
local val = redis.call('incr', key1)
local ttl = redis.call('ttl', key1)
--获取ARGV内的参数并打印
local expire = ARGV[1]
local times = ARGV[2]
redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))
redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
if val == 1 then
redis.call('expire', key1, tonumber(expire))
else
if ttl == -1 then
redis.call('expire', key1, tonumber(expire))
end
end
if val > tonumber(times) then
return 0
end
return 1
启动项目 测试 有效
PS:如果多个模块中的接口都要限流的话 则需要整理成一个 starter 避免写重复代码
自定义starter 源码地址:https://gitee.com/love_yu_0698/ratelimter-spring-boot-starter.git