分布式环境 限流解决方案

  • 业务背景介绍
    对于web应用的限流,光看标题,彷佛过于抽象,难以理解,那咱们仍是以具体的某一个应用场景来引入这个话题吧。
    在平常生活中,咱们确定收到过很多很多这样的短信,“双11约吗?,千款….”,“您有幸得到唱读卡,赶快戳连接…”。这种类型的短信是属于推广性质的短信。为何我要说这个呢?听我慢慢道来。
    通常而言,对于推广营销类短信,它们针对某一群体(譬如注册会员)进行定点推送,有时这个群体的成员量比较大,譬如京东的会员,能够达到千万级别。所以相应的,发送推广短信的量也会增大。然而,要完成这些短信发送,咱们是须要调用服务商的接口来完成的。假若一次发送的量在200万条,而咱们的服务商接口每秒能处理的短信发送量有限,只能达到200条每秒。那么这个时候就会产生问题了,咱们如何能控制好程序发送短信时的速度昵?因而限流这个功能就得加上了
  • 生产环境背景
    一、服务商接口所能提供的服务上限是400条/s
    二、业务方调用短信发送接口的速度未知,QPS可能达到800/s,1200/s,或者更高
    三、当服务商接口访问频率超过400/s时,超过的量将拒绝服务,多出的信息将会丢失
    四、线上为多节点布置,但调用的是同一个服务商接口
  • 需求分析
    一、鉴于业务方对短信发送接口的调用频率未知,而服务商的接口服务有上限,为保证服务的可用性,业务层须要对接口调用方的流量进行限制—–接口限流
  • 需求设计
    方案1、在提供给业务方的Controller层进行控制。
    一、使用guava提供工具库里的RateLimiter类(内部采用令牌捅算法实现)进行限流
<!--核心代码片断--> private RateLimiter rateLimiter = RateLimiter.create(400);//400表示每秒容许处理的量是400 if(rateLimiter.tryAcquire()) { //短信发送逻辑能够在此处 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

二、使用Java自带delayqueue的延迟队列实现(编码过程相对麻烦,此处省略代码)java

三、使用redis实现,存储两个key,一个用于计时,一个用于计数。请求每调用一次,计数器增长1,若在计时器时间内计数器未超过阈值,则能够处理任务web

if(!cacheDao.hasKey(API_WEB_TIME_KEY)) { cacheDao.putToValue(API_WEB_TIME_KEY,0,(long)1, TimeUnit.SECONDS); } if(cacheDao.hasKey(API_WEB_TIME_KEY)&&cacheDao.incrBy(API_WEB_COUNTER_KEY,(long)1) > (long)400) { LOGGER.info("调用频率过快"); } //短信发送逻辑 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

方案2、在短信发送至服务商时作限流处理
方案3、同时使用方案一和方案二redis

  • 可行性分析
    最快捷且有效的方式是使用RateLimiter实现,可是这很容易踩到一个坑,单节点模式下,使用RateLimiter进行限流一点问题都没有。可是…线上是分布式系统,布署了多个节点,并且多个节点最终调用的是同一个短信服务商接口。虽然咱们对单个节点能作到将QPS限制在400/s,可是多节点条件下,若是每一个节点均是400/s,那么到服务商那边的总请求就是节点数x400/s,因而限流效果失效。使用该方案对单节点的阈值控制是难以适应分布式环境的,至少目前我还没想到更为合适的方式。
    对于第二种,使用delayqueue方式。其实主要存在两个问题,1:短信系统自己就用了一层消息队列,有用kafka,或者rabitmq,若是再加一层延迟队列,从设计上来讲是不太合适的。2:实现delayqueue的过程相对较麻烦,耗时可能比较长,并且达不到精准限流的效果
    对于第三种,使用redis进行限流,其很好地解决了分布式环境下多实例所致使的并发问题。由于使用redis设置的计时器和计数器均是全局惟一的,无论多少个节点,它们使用的都是一样的计时器和计数器,所以能够作到很是精准的流控。同时,这种方案编码并不复杂,可能须要的代码不超过10行。算法

  • 实施方案
    根据可行性分析可知,整个系统采起redis限流处理是成本最低且最高效的。
    具体实现markdown

    一、在Controller层设置两个全局key,一个用于计数,另外一个用于计时并发

private static final String API_WEB_TIME_KEY = "time_key"; private static final String API_WEB_COUNTER_KEY = "counter_key";
  • 1
  • 2
  • 3

二、对时间key的存在与否进行判断,并对计数器是否超过阈值进行判断分布式

if(!cacheDao.hasKey(API_WEB_TIME_KEY)) { cacheDao.putToValue(API_WEB_TIME_KEY,0,(long)1, TimeUnit.SECONDS); cacheDao.putToValue(API_WEB_COUNTER_KEY,0,(long)2, TimeUnit.SECONDS);//时间到就从新初始化为 } if(cacheDao.hasKey(API_WEB_TIME_KEY)&&cacheDao.incrBy(API_WEB_COUNTER_KEY,(long)1) > (long)400) { LOGGER.info("调用频率过快"); } //短信发送逻辑
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

实施结果
能够达到很是精准的流控,截图会在后续的过程当中贴出来。欢迎有疑问的小伙伴们在评论区提出问题,我看到后尽可能抽时间回答的工具

相关文章
相关标签/搜索