限流实现方案探讨

2022-09-13 23:22:44阅读 25评论 0

限流算法在分布式领域是⼀个经常被提起的话题,当系统的处理能⼒ 有限时,如何阻⽌计划外的请求继续对系统施压,这是⼀个需要重视 的问题。

除了控制流量,限流还有⼀个应⽤⽬的是⽤于控制⽤户⾏为,避免垃圾请求。⽐如在 UGC 社区,⽤户的发帖、回复、点赞等⾏为都要严 格受控,⼀般要严格限定某⾏为在规定时间内允许的次数,超过了次 数那就是⾮法⾏为。对⾮法⾏为,业务必须规定适当的惩处策略。

定义接口

public interface BehaviorContollerInterface {
    /**
     *
     * @param userId 用户id
     * @param action 行为
     * @param period 特定时间内,秒
     * @param maxCount 发生次数
     * @return
     */
    boolean isActionAllowed(String userId,String action,int period,int maxCount);
}

1)基于redis的实现

package com.example.demo;

import org.springframework.data.redis.core.RedisTemplate;

import java.time.Duration;

public class NormalBehaviorController implements BehaviorContollerInterface{

    RedisTemplate redisTemplate;

    public NormalBehaviorController(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean isActionAllowed(String userId, String action, int period, int maxCount) {
        String key = String.format("hit:%s:%s",userId,action);
        long nowTs = System.currentTimeMillis();
        //记录行为
        redisTemplate.opsForZSet().add(key,nowTs,nowTs);
        //通过设置过期时间自动剔除掉时间窗口外的数据
        redisTemplate.expire(key, Duration.ofMillis(period*1000));
        //比较时间窗口内数据条数和设置的数据条数
        return redisTemplate.opsForZSet().zCard(key)<=maxCount;
    }
}

这种⽅案也有缺点,因 为它要记录时间窗⼝内所有的⾏为记录,如果这个量很⼤,⽐如限定 60s 内操作不得超过 100w 次这样的参数,它是不适合做这样的限流的,因为会消耗⼤量的存储空间。

2)漏斗算法实现

漏⽃的容量是有限的,如果将漏嘴堵住,然后⼀直往⾥⾯灌⽔,它就 会变满,直⾄再也装不进去。如果将漏嘴放开,⽔就会往下流,流⾛ ⼀部分之后,就⼜可以继续往⾥⾯灌⽔。如果漏嘴流⽔的速率⼤于灌 ⽔的速率,那么漏⽃永远都装不满。如果漏嘴流⽔速率⼩于灌⽔的速 率,那么⼀旦漏⽃满了,灌⽔就需要暂停并等待漏⽃腾空。

所以,漏⽃的剩余空间就代表着当前⾏为可以持续进⾏的数量,漏嘴 的流⽔速率代表着系统允许该⾏为的最⼤频率。

package com.example.demo;

import java.util.HashMap;
import java.util.Map;

public class FunnelBehaviorController implements BehaviorContollerInterface {
    static Map<String,Funnel> funnelMap = new HashMap();
    static class Funnel{
        public Funnel(int capacity, float leakingRate, int leftQuota) {
            this.capacity = capacity;
            this.leakingRate = leakingRate;
            this.leftQuota = leftQuota;
            this.leakingTs = System.currentTimeMillis();
        }

        /**
         * 灌水
         * @param quota
         * @return
         */
        public boolean watering(int quota){
            makingSpace();
            if( leftQuota >= quota ){
                leftQuota -= quota;
                return true;
            }
            return false;
        }

        /**
         * 计算漏水量并腾出空间
         */
        private void makingSpace(){
            long nowTs = System.currentTimeMillis();
            long deltaTs = (nowTs - leakingTs)/1000;
            int deltaQuota = (int) (deltaTs * leakingRate);
            if( deltaQuota < 1 ){
                return;
            }
            if( deltaQuota + leftQuota > capacity ){
                leftQuota = capacity;
                return;
            }
            leftQuota += deltaQuota;
            leakingTs = System.currentTimeMillis();
        }

        /**
         * 漏斗容量
         */
        int capacity;
        /**
         * 漏水速率 单位:次/秒
         */
        float leakingRate;
        /**
         * 漏斗剩余空间 单位:次
         */
        int leftQuota;
        /**
         * 上一次漏水时间
         */
        long leakingTs;
    }
    @Override
    public boolean isActionAllowed(String userId, String action, int period, int maxCount) {
        String key = String.format("hit:%s:%s",userId,action);
        float leakingRate = maxCount/period;
        Funnel funnel = null;
        if(funnelMap.get(key)!=null){
            funnel = funnelMap.get(key);
        }else {
            funnel = new Funnel(maxCount,leakingRate,maxCount);
            funnelMap.put(key,funnel);
        }
        return funnel.watering(1);
    }
}

但是有个问题,我们⽆法保证整个过程的原⼦性。

3)最终方案

Redis 4.0 提供了⼀个限流 Redis 模块,它叫 redis-cell。该模块使⽤了令牌桶算法,并提供了原⼦的限流指令。

CL.THROTTLE u1:apply 15 30 60 1
               ▲     ▲  ▲  ▲ ▲
               |     |  |  | └───── apply 1 token (default if omitted)
               |     |  └──┴─────── 30 tokens / 60 seconds
               |     └───────────── 15 max_burst
               └─────────────────── key "u1:apply"

https://github.com/brandur/redis-cell

还没有评论,来说两句吧