Reids 简单流控

  • 流控是分布式领域一个被经常用到的一个计数,当系统承载能力有限的时候,如何组织计划外的请求继续对系统施加压力,这是一个需要解决的问题,在系统承载达到峰值的时候,我们需要弃车保帅,保证主流程业务的通畅,除了流控,限流还有一个目的,控制用户行为,避免垃圾请求以及屏蔽某些爬虫软件爬取数据。
如何使用Redis进行流控
  • 一个简单的,常见的案例。系统需要限定某个用户的某个行为在指定时间内只能允许发生N次,如何使用Redis的数据结构来实现,我们限定义如下一个接口:
//指定用户userId, 某个接口(行为)actionKey,指定时间time, 请求次数maxcount
boolean iaActionAllowed(Long userId, String actionKey, Long time, Long maxCount);
  • 通过以上我们可以得出一个简单的实现方案,在接口A内,每次userId请求,将key进行累加,并且设置key过期时间time,当value > maxCount 则阻断。如下简单版本的流控:
public class SimpleRateLimit {
    public static Long time = 30L;
    public static Boolean isActionAllowed(Long userId, String actionKey, Long maxCount) {
        String key = actionKey + "_" + userId.toString();
        Jedis jedis = JedisUtils.getJedis();
        if (jedis.exists(key)) {
            Long times = jedis.incr(key);
            return times > maxCount;
        }else {
            jedis.set(key, "1");
            jedis.expire(key, time.intValue());
        }
        return true;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            if(isActionAllowed(123L, "login", 30L)){
                System.out.println("allowed this action login : " + i);
            }else {
                System.out.println("close the door : "+ i);
            }
        }
    }
}
  • 以上是一个简单版本的流控,只能正对某一个接口功能进行控制,但是这种方式也有一定的弊端,就是我们每次请求都需要判断,判断,虽然Redis的性能比较高,但是每个接口都需要过一次这个逻辑,并且不同的接口我们要用不同的key,整体流控也需要做另外的key,就是还有一定的提升空间了
漏斗限流
  • 漏斗限流是最常用的流控方法,他相关的算法有令牌桶算法,漏桶算法。
  • 如下面图中所示,漏斗的容量是有先的,如果将漏嘴堵住,一直灌水,就满了,直到装不进去。打开漏嘴,水下流。灌水速度大于流速,满了就需要等待,灌水速度小于流速,永远也满不了。所以漏斗的剩余空间代表这当前行为可以持续进行的数量,漏嘴的流速代表系统允许该行为的最大频率。
  • 或者这样理解,向漏斗加水相当于添加令牌,每次请求需要获取一个令牌,漏斗中令牌数相当于系统当前行为可以持续进行的数量,获取令牌的速度代表系统该欣慰的最大频率。如下代码
/**
 * 有点像令牌桶的漏桶
 * @author liaojiamin
 * @Date:Created in 16:57 2020/5/29
 */
public class FunnelRateLimiter {
    private Map<String, Funnel> funnels = new HashMap<>();

    public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate){
        String key = String.format("%s:%s", userId, actionKey);
        Funnel funnel = funnels.get(key);
        if(funnel == null){
            funnel = new Funnel(capacity, leakingRate);
            funnels.put(key, funnel);
        }
        return funnel.watering(1);
    }
    static class Funnel{
        //总量
        int capacity;
        //流速
        float leakingRate;
        //漏桶现有配额
        int leftQuota;
        //开始时间
        long leakingTs;

        public Funnel(int capacity, float leakingRate){
            this.capacity = capacity;
            this.leakingRate = leakingRate;
            this.leftQuota = capacity;
            this.leakingTs = System.currentTimeMillis();
        }
        //获取空间(更新当前桶中的令牌数量,依据时间以及流速)
        void makeSpace(){
            long nowTs = System.currentTimeMillis();
            long deltaTs = nowTs - leakingTs;
            //计算这段时间的总流量 时间差* 流速
            int deltaQuota = (int) (deltaTs * leakingRate);
            //int类型越界情况 重新初始化
            if(deltaQuota < 0){
                this.leftQuota = capacity;
                this.leakingTs = nowTs;
                return;
            }
            //腾出空间太小,最小单位是1
            if(deltaQuota < 1){
                return;
            }
            this.leftQuota += deltaQuota;
            this.leakingTs =nowTs;
            if(this.leftQuota > this.capacity){
                this.leftQuota = this.capacity;
            }
        }
        //消耗存储
        boolean watering(int quota){
            makeSpace();
            if(this.leftQuota >= quota){
                this.leftQuota -= quota;
                return true;
            }
            return false;
        }
    }
}
  • 如上代码,funnel对象的Mask_space是令牌桶核心算法,每次消耗之前都会计算一次桶中的令牌,一次可能消耗一个令牌,当并发请求,一秒可能消耗多个令牌,计算桶中令牌数根据时间固定速率添加,最大值capacity。这样就有一个令牌桶算法。
  • 那么我们怎么用Redis来实现一个令牌桶,有Redis的数据结构能搞定没。
  • 我们可以用hash结构,添加令牌时候将Hash字段取出运算,在更新,这个问题在于非原子操作,非线程安全的。如果考虑事务,就有失败,重试的情况使代码变得更加复杂,得不偿失。
Redis-Cell
  • Redis 4.0 提供来一个流控的Redis模块,他叫Redis-Cell。这个模块也用了漏斗算法,并提供来原子的限流指令,有了这个模块,限流可以通过简单命令来解决。
  • 该模块只有一个指令cl.throttle,他的参数和返回值都比较复杂,如下使用方式:
cl.throttle jiamin:reply 15 30 60 1
  • 如上,加入令牌频率为60s最多30个,漏斗初始容量15个令牌,也就是说开始可以连续15个请求,然后才开始受到添加令牌的速率影响。此处令牌添加的速度变成两个参数,替代之前的单个浮点数。用两个参数相除的结果来表示添加令牌的速度更直观。
  • 返回值有几种:
  • 0: 表示允许
  • 1:表示拒绝
  • 15 :漏桶中令牌容量capacity
  • 14: 漏桶中剩余令牌 left_quota
  • -1 如果被拒绝,需要多长时间后再试
  • 2:多长时间后,漏斗完全填满令牌:left_quota == capacity 单位秒
  • 在执行限流指令时候,如果被拒绝,就需要丢弃或者重试,cl.throttle指令考虑得非常周到,连重试的时间都帮你算好了,直接取返回结果数组的第四个值进行sleep既可,如果不想苏泽线程,也可以异步定时任务来重试。

上一篇Redis高级数据结构下一篇Redis分布式锁奥义