RateLimit:限流

RateLimit:限流

what:什么是限流

顾名思义限制流量

why:为什么我们的服务需要限流

  1. 用户量病毒增长
  2. 微博热搜/淘宝双十一
  3. 竞品爬虫
  4. 恶意攻击

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版本的启动器,核心代码如上。目前还不完善,待完善后发布到仓库。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值