RateLimit:限流
what:什么是限流
顾名思义限制流量
why:为什么我们的服务需要限流
- 用户量病毒增长
- 微博热搜/淘宝双十一
- 竞品爬虫
- 恶意攻击
how:如何限流
一般可以根据服务的某项核心指标,如QPS,来决定是否将后续的请求拦截。比如设定某系统1s的QPS阈值为100,当1s内的QPS达到了110,那么差值的10个请求则会被拦截,直接返回503状态码:服务器繁忙。
根据以上结果导向论,又衍生出了如下3套算法:
(1)计数器
1.思想
假设服务设定的最高QPS为100,声明一个计数器counter,在接下来的1s内,每有一个请求则counter+1,如果在这1s内,counter>100,则限流,1s结束后,counter重置清零。
2.缺点
如果在0.59s时QPS达到了100,在1.00s时QPS也达到了100,那么其实在1s内,QPS达到了200,限流GG!
(2)漏桶算法
1.基本思想
想象有一个木桶,以恒定的速度漏水(处理请求),有新的请求来,可以放进桶里,如果桶满了,则直接拒绝请求。
2.优点
可以平滑收到的请求,以恒定的速度处理。
缺点
请求的处理(漏水)有一定的延时性
(3)令牌桶算法
1.基本思想
想象有一个木桶,按一定速率往桶里放令牌,满了令牌则溢出舍弃。每来一个请求则取一个令牌,桶内无令牌可取则拒绝请求
2.优点
完美解决了计数器存在的临界问题,同时突增的QPS只要桶内有令牌就可以访问。
关键代码
1.注解 Limit.java
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
//资源的名字
String name() default "";
//资源的key
String key() default "";
//Key的prefix
String prefix() default "";
//给定的时间段 单位秒
int period() default 1;
//最多的访问限制次数
int count();
//类型
LimitType limitType() default LimitType.CUSTOMER;
}
2.枚举LimitType.java
public enum LimitType {
/**
* 自定义key
*/
CUSTOMER,
/**
* 根据请求者IP
*/
IP;
}
3.切面LimitAspect.java
@Aspect
@Slf4j
public class LimitAspect {
@Resource(name = "limitRedisTemplate")
private RedisTemplate<String, Serializable> limitRedisTemplate;
@Pointcut("@annotation(com.workbei.ratelimit.spring.boot.limit.Limit)")
public void limitAnnotationPointcut() {
}
@Around("limitAnnotationPointcut()")
public Object interceptor(ProceedingJoinPoint pjp) {
//获得方法上的注解
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
Limit limitAnnotation = method.getAnnotation(Limit.class);
LimitType limitType = limitAnnotation.limitType();
String name = limitAnnotation.name();
String key;
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.count();
switch (limitType) {
case IP:
key = IPUtils.getIpAddr();
break;
case CUSTOMER:
key = limitAnnotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
//加载原子性lua脚本
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
//执行脚本
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
log.info("Access try count is {} for name={} and key = {}", count, name, key);
try {
if (count != null && count.intValue() <= limitCount) {//无需限流:通过
return pjp.proceed();
} else {
throw new RateLimitException("rate limit ing");
}
} catch (Throwable e) {
throw new RateLimitException(e.getMessage());
}
}
/**
* 限流 脚本
*
* @return lua脚本
*/
private String buildLuaScript() {
return ScriptUtils.loader("limit.lua");
}
}
4.limit.lua(redis命令脚本文件)
local c
c = redis.call('get',KEYS[1])
-- 调用不超过最大值,则直接返回
if c and tonumber(c) > tonumber(ARGV[1]) then
return c;
end
-- 执行计算器自加
c = redis.call('incr',KEYS[1])
if tonumber(c) == 1 then
-- 从第一次调用开始限流,设置对应键值的过期
redis.call('expire',KEYS[1],ARGV[2])
end
return c;
计划部署一个springboot版本的启动器,核心代码如上。目前还不完善,待完善后发布到仓库。