由支付防重想到的处理方法

by 董唯良 at 2016.11

背景

昨天的代码评审,有一处支付“防重”功能:用户一分钟之内只能创建一个订单。从而联想到一种互联网项目中比较常用的“限流”功能。 限流可以是应用级的一个限流,也可以是针对某一个具体接口的限流。 应用级别的限流可以通过Nginx、web容器、数据库本身就拥有的某些配置选项来进行。这里主要讨论的是接口的限流。

实现方式

就一天时间,粗暴简单点的方法

原子类型的计数方式,如ActomicLong。预定一个数值,请求总数多余这个数值则采取一些拒绝措施。

try {
  if(atomic.incrementAndGet() > 限流数) {
      //拒绝请求
  }
  //处理请求
} finally {
  atomic.decrementAndGet();
}

计数器做法最为简单,但在一些特殊场景也会比较有效。比如通常的抢购、秒杀业务,超过一个阈值就通知客户无货,或者等待。而有些开发平台,比如百度云,会限制用户的一个访问量,用计数器的方法也是可以的。

Guava是个好东西

LoadingCache<Long, AtomicLong> counter =
      CacheBuilder.newBuilder()
              .expireAfterWrite(2, TimeUnit.SECONDS)
              .build(new CacheLoader<Long, AtomicLong>() {
                  @Override
                  public AtomicLong load(Long seconds) throws Exception {
                      return new AtomicLong(0);
                  }
              });
long limit = 1000;
while(true) {
  //得到当前秒
  long currentSeconds = System.currentTimeMillis() / 1000;
  if(counter.get(currentSeconds).incrementAndGet() > limit) {
      System.out.println("限流了:" + currentSeconds);
      continue;
  }
  //业务处理
}

此处取当前的秒作为key,过期时间为2秒,如果请求数在这2秒内大于了限流数,则就拒绝之后的服务了。 Guava的cache多被用来存放缓存,因为其功能比较强大,包括并发问题,缓存失效机制,内存不够用时缓存释放,缓存的命中率,缓存的移除等都被google考虑到了。

 LoadingCache<Integer,AtomicLong> alCache
    //CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
    = CacheBuilder.newBuilder()
    //设置并发级别为8,并发级别是指可以同时写缓存的线程数
    .concurrencyLevel(8)
    //设置写缓存后8秒钟过期
    .expireAfterWrite(8, TimeUnit.SECONDS)
    //设置缓存容器的初始容量为10
    .initialCapacity(10)
    //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
    .maximumSize(100)
    //设置要统计缓存的命中率
    .recordStats()
    //设置缓存的移除通知
    .removalListener(new RemovalListener<Object, Object>() {
        @Override
        public void onRemoval(RemovalNotification<Object, Object> notification) {
            System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
        }
    })
    //build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
    .build();

//统计命中率
System.out.println(alCache.stats().toString());

这种方法可以用来限制某个服务每秒、每分钟、每小时的访问量。 但是以上这两种方法的使用无法解决请求“平滑”的问题,还是会允许一定的突发请求,造成TPS曲线忽高忽低的现象。

令牌桶、漏桶

目前的限流保护中比较重要的两种算法就是这两个“桶”了。 这两种算法主要是用来平衡请求的一个速率,让突发请求按照我要求的速度来访问。

令牌桶

令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下: 假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌; 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝; 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上; 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

漏桶

漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下: 一个固定容量的漏桶,按照常量固定速率流出水滴; 如果桶是空的,则不需流出水滴; 可以以任意速率流入水滴到漏桶; 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

两只桶的区别

令牌桶里面装载的是令牌,然后让令牌去关联到数据发送,常规漏桶里面装载的是数据,令牌桶允许用户的正常的持续突发量,就是一次就将桶里的令牌全部用尽的方式来支持续突发,而常规的漏桶则不允许用户任何突发行。因此令牌桶对于用户来讲比较友好,基本上都是使用令牌桶来进行限流操作。

实现方式

魅族内部已经有系统使用了令牌桶的方式进行了限流保护, Push平台基于令牌桶的流量控制实现

然后Guava又来了(以后要好好分析下Guava的包!),Google提供了现成的API,让我们能够非常方便的使用令牌桶算法来进行限流保护。 Guava RateLimiter提供了令牌桶算法实现:平滑突发限流 (SmoothBursty) 和平滑预热限流 (SmoothWarmingUp)实现。

SmoothBursty

RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

输出

0.0
0.199505
0.198057
0.20024
0.20025
  • RateLimiter.create(5)表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌;

  • limiter.acquire()表示消费一个令牌,如果当前桶中有足够令牌则成功(返回值为0),如果桶中没有令牌则暂停一段时间,比如发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌(如上测试用例返回的为0.198239,差不多等待了200毫秒桶中才有令牌可用),这种实现将突发请求速率平均为了固定请求速率。

Last updated