分布式限流大作战

开篇

  一周一篇技术博文又来了,这周我们讲点什么呢?看标题就知道了,那就是分布式下的限流策略(实在不知道写些什么好呢),至于限流的用处,好处,和处理场景就不这里赘述了(Google全是)。ok,凌云小课堂正式开始啦,今天要介绍的主要是三种限流策略

1. 计数限流  
2. 漏桶限流  
3. 令牌桶限流 

  基本上述三种限流策略可以涵盖大多数需要限流的场景。为了和广大的技术博主不一样(555),我们今天只讲分布式下的限流实现,至于什么是漏桶和令牌桶Google喽。。。

计数限流(时间窗)

  计数限流主要有两种,一个是计数器限流,简单粗暴不值一提,另一个就是本文要讲的滑动时间窗限流法,我们粗暴的贴代码。。。

/**
     * 简单时间窗限流策略:
     * 每X秒允许行为发生Y次
     *
     * @param key
     * @param period
     * @param maxCount
     * @return
     */
    public boolean isActionAllowed(String key, int period, int maxCount) {
        ShardedJedis jedis = pool.getResource();
        long nowTs = System.currentTimeMillis();
        try {
            ShardedJedisPipeline pipe = jedis.pipelined();
            // 移除时间窗之前所有集合
            pipe.zremrangeByScore(key, 0f, new Double(nowTs - period * 1000));
            // 获取窗口内的行为数量
            Response<Long> count = pipe.zcard(key);
            pipe.sync();
            // 是否超过限制
            if (count.get() <= maxCount) {
                // 记录行为
                pipe.zadd(key, nowTs, "" + nowTs);
                // 设置zset过期时间,避免冷用户持续占用内存,过期时间等于时间窗口,再多宽限 1s
                pipe.expire(key, period + 1);
                pipe.sync();
                return true;
            }
        } catch (Exception e) {
            log.error("redis timeLimit:Error", e);
        } finally {
            jedis.close();
        }
        return false;
    }

  这个限流策略是基于Redis实现的。主要精髓就是利用zset的score数学特性,将时间戳存于score中。处理逻辑如下

1.每次进件先移除时间窗(给定的时间周期)外的所有key
2.对比当前zset中key的总量与MaxCount确定是否放弃本次进件
3.如果同意zset中新增value
4.设置过期时间

  这个实现方式在并发量不大的应用中是完全可以应付的,但是一旦并发量过大在第2步和第3步之间因为不是原子操作,所以可能出现key的总量突破最大限流数。所以一定要用在合适的场景中。为什么不用lua实现原子操作呢?因为ShardedJedis不支持。。。默默哭泣中。

漏桶限流-令牌桶限流

  漏桶和令牌桶是一对孪生兄弟,它两都是流量整形,限流中的常用算法,唯一区别呢就是令牌桶是允许突发流量的而漏桶严格限制流量速率。他们的实现甚至可能一摸一样。。。
  我们继续暴力贴代码。。555


/**
 * 漏斗限流
 *
 * @author Lingyun
 * @Date 2018-12-08 21:48
 */
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); // 需要1个quota
    }

    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);//可腾出空间

            if (deltaQuota < 0) { // 间隔时间过长,整数数字过大溢出
                this.leftQuota = capacity;
                this.leakingTs = nowTs;
                return;
            }
            if (deltaQuota < 1) { // 可腾出空间过小,最小单位是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;
        }
    }
}

  以上就是漏桶限流的java实现,至于令牌桶限流代码,Google的Guava中有该算法的实现(RateLimiter)使用还是非常简单的(意思就是自行Google)。
  说好的分布式呢。。。

漏桶算法的分布式实现思路

  将Funnel 对象的内容按字段存储到一个 hash 结构中,进水的时候将 hash 结构的字段取出来进行逻辑运算后,再将新值回填到 hash 结构中就完成了一次行为频度的检测。

  但是有个问题,我们无法保证整个过程的原子性。从 hash 结构中取值,然后在内存里运算,再回填到 hash 结构,这三个过程无法原子化,意味着需要进行适当的加锁控制。而一旦加锁,就意味着会有加锁失败,加锁失败就需要选择重试或者放弃。

  如果重试的话,就会导致性能下降。如果放弃的话,就会影响用户体验。同时,代码的复杂度也跟着升高很多。这真是个艰难的选择,我们该如何解决这个问题呢?救星来了!我们可以使用Redis-Cell,可惜的是Redis-Cell是一个模块,而模块特效是Redis4.0中带来的。。。而公司的Redis还是可怜的2.X版本。。。

令牌桶分布式实现

  我们公司使用的接口限流策略就是令牌桶限流。实现方式是Guava的RateLimit+zookeeper,zookeeper中存储了网关中心的服务数量,各个服务均分接口流量,并对网关中心最高QPS也在每个服务器上做均分。这样的好处是限流令牌在本地保存,不通过网络传输,每台服务器绝对的均分限流,当集群中有服务下线也不会影响其他服务器的限流阈值,不会因为一台服务的下线流量集中导致其他服务的限流阈值上升,继而出现雪崩。缺点就是不灵活,在高并发时负载不均衡的情况肯定会出现,但是因为每台服务器的限流阈值是一个定值,这就导致某些压力较大的服务不能灵活的根据整个集群的限流情况,调整限流阈值,只能拒绝服务。
  暴力贴代码又来了。。。


   /**
     * 限流校验
     *
     * @param key 限流缓存对象key
     * @return 校验通过返回true 反之返回false
     */
    private boolean checkRateLimit(String key, Integer limiterConcurrency, Integer limiterTimeOut) {
        boolean valid = true;
        try {
            long startTime = System.currentTimeMillis();
            int tmpCount = serverCount == 0 ? DEFAUL_SERVER_COUNT : serverCount;
            double permitsPerSecond = BigDecimal.valueOf(limiterConcurrency).divide(BigDecimal.valueOf(tmpCount), 2, RoundingMode.FLOOR).doubleValue();
            RateLimiter rateLimiter = getRateLimiter(key, permitsPerSecond);
            if (limiterTimeOut == null || limiterTimeOut == 0) {
                valid = rateLimiter.tryAcquire();
            } else {
                valid = rateLimiter.tryAcquire(limiterTimeOut, TimeUnit.MILLISECONDS);
            }
            long endTime = System.currentTimeMillis();
            LOGGER.info(String.format("api接口限流校验,key=%s,valid=%s,serverCount=%s,耗时=%s", key, valid, tmpCount, endTime - startTime));
        } catch (Throwable e) {
            LOGGER.error(String.format("api接口限流校验异常,key=%s", key), e);
        }
        return valid;
    }

    /**
     * 获取限流对象
     *
     * @param key              限流对象缓存KEY
     * @param permitsPerSecond
     * @return
     */
    private RateLimiter getRateLimiter(String key, double permitsPerSecond) {
        RateLimiter rateLimiter = rateLimiterMap.get(key);
        if (rateLimiter == null) {
            synchronized (this) {
                rateLimiter = rateLimiterMap.get(key);
                if (rateLimiter == null) {
                    rateLimiter = RateLimiter.create(permitsPerSecond);
                    rateLimiterMap.put(key, rateLimiter);
                    return rateLimiter;
                }
            }
        }
        double diff = permitsPerSecond - rateLimiter.getRate();
        if (diff >= 0.01 || diff < 0) {
            LOGGER.info(String.format("api接口限流-并发量更新,key=%s,permitsPerSecond=%s", key, permitsPerSecond));
            rateLimiter.setRate(permitsPerSecond);
        }
        return rateLimiter;
    }

又要快乐的结束了.....

  写到这里时,分布式限流终于要结束了,开开心心的看了看(>﹏<)左耳朵耗子的限流设计博文,嗯,我真是渣.....

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容