分布式限流算法演进过程

限流算法

一. sentx + incr

​ 谈起限流算法,记得之前做过一个限制用户获取手机验证码次数的需求,同一个手机号24小时最多只能获取20次手机验证码,防止接口被盗刷。当时使用当前时间(yyyy-MM-dd) + 当前用户唯一标识作为 key,使用setnx + incr实现,当value的值达到20,拒绝发送短信。时间思路和代码都比较简单,不在此赘述了。

​ 这种方案有一个致命的缺陷,如果限制间隔10分钟用户只能请求5次,假设再1-10 分钟请求9次,10-11分钟请求9次,那么此用户在1-11分钟请求了18次,与需求相违背。不可取。

二. zset 滑动窗口

​ redis提供了sorted set 数据结构,即zset,zadd key score member ,它支持按照score对set中的元素排序,那么我们可以将当前请求的请求时间时间戳作为score,使用uuid作为value(member), 那么我们要统计某个时间段内的访问书数据个数就易如反掌。

@Target({ElementType.METHOD, ElementType.TYPE}) // 可以考虑应用到类上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LimitFlow {
    /**
     * 限流间隔
     * @return
     */
    int interval() default 60 * 1000;

    /**
     * 在interval内可以执行的次数
     * @return
     */
    int limit() default 10;
}



@Aspect
@Component
public class LimitFlowAspect {

    @Pointcut("@annotation(com.cph.aspect.LimitFlow)")
    private void pointCutMethodController() {
    }

    /**
     * 环绕通知前后增强
     */
    @Around(value = "pointCutMethodController()")
    public Object doAroundService(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        LimitFlow limitFlow = method.getAnnotation(LimitFlow.class);
        if (limitFlow != null) {
            int interval = limitFlow.interval();
            int limit = limitFlow.limit();

            if(!RedisUtils.limitFlow("limit", limit, interval)){
                return new CommonResult(500, "操作频繁,请稍后再试", null, null);
            }
        }
        Object result = joinPoint.proceed();
        return result;
    }
}



/**
 * 限流
 * @param key
 * @param limit
 * @param interval
 */
public static Boolean limitFlow(String key, int limit, int interval){
    Long currentTime = new Date().getTime();
    if (redisTemplate.hasKey(key)) {
        Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime - interval, currentTime).size();
        if( count > limit) return false;
    }
    redisTemplate.opsForZSet().add(key, UUID.randomUUID().toString(), currentTime);
    return true;
}

滑动窗口解决了 一. sentx + incr 某个时间段内的访问数据的问题。

三. 令牌桶算法

每个请求执行之前都需要从桶中获取令牌,只有获取到令牌的请求才可以执行,否则拒绝执行。使用redis中的list实现,启动一个定时任务定期向令牌桶放入令牌,桶满了则丢弃。当请求来的时候获取令牌,当令牌桶为空则证明无法获取令牌,拒绝执行。

public Response limitFlow2(Long id){
        Object result = redisTemplate.opsForList().leftPop("limit_list");
        if(result == null){
            return Response.ok("当前令牌桶中无令牌");
        }
        return Response.ok(articleDescription2);
    }

@Scheduled(fixedDelay = 10_000,initialDelay = 0)
public void setIntervalTimeTask(){
    redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
}

四. 漏斗算法

漏斗算法类似于将请求装在一个漏斗中,按照固定速率进行消费,漏斗满了之后溢出的请求拒绝服务。

五. Redission

redission 的 RRateLimiter 使用 lua脚本 + 令牌桶算法实现。

RedissonClient redissonClient = Redisson.create();
RRateLimiter rateLimiter = redissonClient.getRateLimiter("xindoo.limiter");
rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.HOURS); 
rateLimiter.acquire(1); // 申请1份许可,直到成功
boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS); // 申请1份许可,如果5s内未申请到就放弃
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值