限流系统稳定基石

为什么要限流

  秒杀时特别热卖的商品,会有很多人来抢(不排除黄牛),不进行限流,有可能把缓存和DB全部拖垮从而造成雪崩。
  微博上的热搜,那流量可想而知。
  比如业务方发布了一个活动,拼团支付一分钱,抽百万大奖,这是拼团支付流量很大,影响了其他业务的正常支付,这个时候需要对各个业务方做单独限流。
  业务方提出需求,用户在单位时间内访问某个页面的次数不能超过多少,这时也需要限流。
  限流应用场景很多,就不一一列举了,这些应用场景应该单独抽取出来,做成一个流控服务。

常见的限流有 : 单机限流,分布式限流,计数器限流。

单机限流

  单机限流的常见算法 漏桶算法、令牌桶算法、计数器算法

漏桶算法

  由图片可知,桶的漏出速率是一定的,请求达到桶的容量之后就会拒绝请求。漏桶算法有一定的缺点,就是当系统可以承受这么大的压力时,请求放过的量还是这么多,不能有效的利用系统资源。


漏桶算法

令牌桶算法

  如下图所示,请求过来以后先去桶里拿令牌,拿不到令牌之后则拒绝请求。令牌桶允许一定量的突发流量,请求空闲时预热一部分令牌,新请求无需等待


令牌桶算法

  guava 的RateLimiter默认就是令牌桶实现,提供了SmoothWarmingUp(预热限流),SmoothWarmingUp(突发限流)两种实现。
几个关键的成员变量

  // 当前桶里存放多少个令牌
  double storedPermits;
  // 最大可以存放多少令牌
  double maxPermits;
  // 获取一个令牌需要的时间,比如限流qps是5,那此值就是 1/5 = 200ms
  double stableIntervalMicros;
  // 记录最近一个可以获取令牌的时间。
  long nextFreeTicketMicros = 0L;
public double acquire(int permits) {
    // 拿 permits 个令牌要等待多长时间
    long microsToWait = reserve(permits);
    // 其实就是sleep
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

接下来看一下reserve方法的实现

final long reserve(int permits) {
    checkPermits(permits);
    // mutex方法使用了double check,这在多线程编程里是很常见的
    synchronized (mutex()) {
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
  }

reserveAndGetWaitLength 调用了reserveEarliestAvailable方法,最主要的实现就是在这个方法里了。没看源码之前总以为是有一个后台线程在不断的往桶里面放令牌,其实是每次拿令牌的时候才去更新桶里的令牌数量,这个实现很精髓。

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
   // 这个方法的作用是,假如有很长时间都没有线程来拿令牌了,则更新令牌桶里的令牌数量
    resync(nowMicros);
    long returnValue = nextFreeTicketMicros;
    // 取要求拿几个令牌 和 现有存储令牌的 最小值
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    double freshPermits = requiredPermits - storedPermitsToSpend;
    // 计算等待时间,storedPermitsToWaitTime 在SmoothBursty实现里返回0
    // 当拿的令牌数量小于桶中剩余令牌数量时,等待时间为0。
    long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
        + (long) (freshPermits * stableIntervalMicros);
    // 更新剩余令牌数量和最近一个可用令牌的时间
    this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }

返回的这个returnValue 大于0的话,当前线程就sleep多长时间。

private void resync(long nowMicros) {
    // 当前时间大于最后一个令牌可用时间,说明有一段时间没有线程来拿令牌了
    if (nowMicros > nextFreeTicketMicros) {
      // 更新存储令牌的数量,计算方式如下
      storedPermits = min(maxPermits,
          storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
      nextFreeTicketMicros = nowMicros;
    }
  }

acquire方法介绍完了,接下来在看一下 tryAcquire 方法的实现。

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
    long timeoutMicros = max(unit.toMicros(timeout), 0);
    checkPermits(permits);
    long microsToWait;
    // acquire 和 tryAcquire 方法在此处都是加锁线程安全的。
    synchronized (mutex()) {
      long nowMicros = stopwatch.readMicros();
      // 看能否获取锁
      if (!canAcquire(nowMicros, timeoutMicros)) {
        return false;
      } else {
        // 接下来就是调用 reserveEarliestAvailable方法,上面已经介绍过了
        microsToWait = reserveAndGetWaitLength(permits, nowMicros);
      }
    }
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return true;
  }
// timeoutMicros 参数就是最长等待多长时间
private boolean canAcquire(long nowMicros, long timeoutMicros) {
    // queryEarliestAvailable 返回的是 nextFreeTicketMicros
    // 最近一次可以获取令牌的时间 - 等待时间 < 当前时间
    return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
 }

guava的RateLimiter源码就大致解析到这里

计数器算法

这个实现比较简单,demo如下

// 设置1秒钟过期
LoadingCache<String, AtomicLong> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, AtomicLong>() {
            @Override
            public AtomicLong load(String key) throws Exception {
                // load 方法内部加了 synchorized 代码块修饰,在高并发下key失效,也只有一个线程调用此方法去加载初始值放到cache,其余线程从cache获取
                return new AtomicLong(0);
            }
        });
 if (cache.get("key").incrementAndGet() < 限流大小) {
      // pass
  } else {
      // reject
  }

单机限流还可以根据 线程池,每个限流方法配置不同的线程池,做到很好的线程隔离。也可以使用信号量,hystrix实现。

分布式限流

原因

  现在服务部署都是集群方式,使用比如dubbo等RPC框架,还有使用nginx做反向代理,有可能流量请求到物理机上不均匀,还有就是机器的性能不相同,如果使用单机限流的话,性能好的物理机资源会大大的浪费。

应用

  • 支付服务是核心服务,会有多个业务方来调,这时候要多各个业务方做限流,防止由于某个业务方支付量激增时影响其他业务方支付。
  • 限制 百分之多少的用户请求时直接降级。
  • 限制用户一天访问多少次,等等。

实现

  • 分布式限流可以基于redis,对限流的key做incr操作,并设置过期值,当incr的值大于限流值之后,直接reject调。
  • 可以单独部署一个令牌服务,每次请求时客户端去令牌服务端拿对应的令牌,令牌服务做成HA高可用,这样就不强依赖redis。(阿里开源的sentinel分布式限流就是这么做的)
  • 也可以使用nginx做限流,nginx+lua脚本实现。

  下面简单画了一下基于redis的集群限流图片


redis限流

高效率与高可用

  • 集群限流强依赖于第三方(比如redis),当网络放生抖动时,会使一个请求的时间大大加长,这对于业务方来说是不可忍受的。可以对请求redis的时间做熔断降级(异步写redis),当redis不可用时,集群限流降级为单机限流(集群限流值/机器总数)。
  • 客户端也可以一次拿多个令牌,这样就不会频繁请求redis了。
  • 读取redis的从库,写入主库,这样有可能存在主从延时导致多放请求进来。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 在系统架构设计当中,限流是一个不得不说的话题,因为他太不起眼,但是也太重要了。这点有些像古代镇守边陲的将士,据守隘...
    Java大生阅读 770评论 0 0
  • 在系统架构设计当中,限流是一个不得不说的话题,因为他太不起眼,但是也太重要了。这点有些像古代镇守边陲的将士,据守隘...
    java菜阅读 357评论 0 4
  • 聊聊高并发系统限流特技-1来自开涛的博客 在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是...
    meng_philip123阅读 6,617评论 1 20
  • 在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可...
    高级java架构师阅读 692评论 0 5
  • 关键词:性欲 题主:女 问:他170cm,25岁,经营货车生意。我跟他联系时,他都有女朋友了,但没结婚。 跟他联系...
    冷爱阅读 20,461评论 0 8