高并发中常见的限流方式
这是java高并发系列第29篇。
环境:jdk1.8。
本文内容
- 介绍常见的限流算法
- 通过控制最大并发数来进行限流
- 通过漏桶算法来进行限流
- 通过令牌桶算法来进行限流
- 限流工具类RateLimiter
常见的限流的场景
- 秒杀活动,数量有限,访问量巨大,为了防止系统宕机,需要做限流处理
- 国庆期间,一般的旅游景点人口太多,采用排队方式做限流处理
- 医院看病通过发放排队号的方式来做限流处理。
常见的限流算法
- 通过控制最大并发数来进行限流
- 使用漏桶算法来进行限流
- 使用令牌桶算法来进行限流
通过控制最大并发数来进行限流
以秒杀业务为例,10个iphone,100万人抢购,100万人同时发起请求,最终能够抢到的人也就是前面几个人,后面的基本上都没有希望了,那么我们可以通过控制并发数来实现,比如并发数控制在10个,其他超过并发数的请求全部拒绝,提示:秒杀失败,请稍后重试。
并发控制的,通俗解释:一大波人去商场购物,必须经过一个门口,门口有个门卫,兜里面有指定数量的门禁卡,来的人先去门卫那边拿取门禁卡,拿到卡的人才可以刷卡进入商场,拿不到的可以继续等待。进去的人出来之后会把卡归还给门卫,门卫可以把归还来的卡继续发放给其他排队的顾客使用。
JUC中提供了这样的工具类:Semaphore,示例代码:
1 package com.itsoku.chat29; 2 3 import java.util.concurrent.Semaphore; 4 import java.util.concurrent.TimeUnit; 5 6 /** 7 * 跟着阿里p7学并发,微信公众号:javacode2018 8 */ 9 public class Demo1 { 10 11 static Semaphore semaphore = new Semaphore(5); 12 13 public static void main(String[] args) { 14 for (int i = 0; i < 20; i++) { 15 new Thread(() -> { 16 boolean flag = false; 17 try { 18 flag = semaphore.tryAcquire(100, TimeUnit.MICROSECONDS); 19 if (flag) { 20 //休眠2秒,模拟下单操作 21 System.out.println(Thread.currentThread() + ",尝试下单中。。。。。"); 22 TimeUnit.SECONDS.sleep(2); 23 } else { 24 System.out.println(Thread.currentThread() + ",秒杀失败,请稍微重试!"); 25 } 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } finally { 29 if (flag) { 30 semaphore.release(); 31 } 32 } 33 }).start(); 34 } 35 } 36 37 }
输出:
Thread[Thread-10,5,main],尝试下单中。。。。。 Thread[Thread-8,5,main],尝试下单中。。。。。 Thread[Thread-9,5,main],尝试下单中。。。。。 Thread[Thread-12,5,main],尝试下单中。。。。。 Thread[Thread-11,5,main],尝试下单中。。。。。 Thread[Thread-2,5,main],秒杀失败,请稍微重试! Thread[Thread-1,5,main],秒杀失败,请稍微重试! Thread[Thread-18,5,main],秒杀失败,请稍微重试! Thread[Thread-16,5,main],秒杀失败,请稍微重试! Thread[Thread-0,5,main],秒杀失败,请稍微重试! Thread[Thread-3,5,main],秒杀失败,请稍微重试! Thread[Thread-14,5,main],秒杀失败,请稍微重试! Thread[Thread-6,5,main],秒杀失败,请稍微重试! Thread[Thread-13,5,main],秒杀失败,请稍微重试! Thread[Thread-17,5,main],秒杀失败,请稍微重试! Thread[Thread-7,5,main],秒杀失败,请稍微重试! Thread[Thread-19,5,main],秒杀失败,请稍微重试! Thread[Thread-15,5,main],秒杀失败,请稍微重试! Thread[Thread-4,5,main],秒杀失败,请稍微重试! Thread[Thread-5,5,main],秒杀失败,请稍微重试!
关于Semaphore的使用,可以移步:JUC中的Semaphore(信号量)
使用漏桶算法来进行限流
国庆期间比较火爆的景点,人流量巨大,一般入口处会有限流的弯道,让游客进去进行排队,排在前面的人,每隔一段时间会放一拨进入景区。排队人数超过了指定的限制,后面再来的人会被告知今天已经游客量已经达到峰值,会被拒绝排队,让其明天或者以后再来,这种玩法采用漏桶限流的方式。
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
漏桶算法示意图:

简陋版的实现,代码如下:
1 package com.itsoku.chat29; 2 3 import java.util.Objects; 4 import java.util.concurrent.ArrayBlockingQueue; 5 import java.util.concurrent.BlockingQueue; 6 import java.util.concurrent.TimeUnit; 7 import java.util.concurrent.atomic.AtomicInteger; 8 import java.util.concurrent.locks.LockSupport; 9 10 /** 11 * 跟着阿里p7学并发,微信公众号:javacode2018 12 */ 13 public class Demo2 { 14 15 public static class BucketLimit { 16 static AtomicInteger threadNum = new AtomicInteger(1); 17 //容量 18 private int capcity; 19 //流速 20 private int flowRate; 21 //流速时间单位 22 private TimeUnit flowRateUnit; 23 private BlockingQueue<Node> queue; 24 //漏桶流出的任务时间间隔(纳秒) 25 private long flowRateNanosTime; 26 27 public BucketLimit(int capcity, int flowRate, TimeUnit flowRateUnit) { 28 this.capcity = capcity; 29 this.flowRate = flowRate; 30 this.flowRateUnit = flowRateUnit; 31 this.bucketThreadWork(); 32 } 33 34 //漏桶线程 35 public void bucketThreadWork() { 36 this.queue = new ArrayBlockingQueue<Node>(capcity); 37 //漏桶流出的任务时间间隔(纳秒) 38 this.flowRateNanosTime = flowRateUnit.toNanos(1) / flowRate; 39 Thread thread = new Thread(this::bucketWork); 40 thread.setName("漏桶线程-" + threadNum.getAndIncrement()); 41 thread.start(); 42 } 43 44 //漏桶线程开始工作 45 public void bucketWork() { 46 while (true) { 47 Node node = this.queue.poll(); 48 if (Objects.nonNull(node)) { 49 //唤醒任务线程 50 LockSupport.unpark(node.thread); 51 } 52 //休眠flowRateNanosTime 53 LockSupport.parkNanos(this.flowRateNanosTime); 54 } 55 } 56 57 //返回一个漏桶 58 public static BucketLimit build(int capcity, int flowRate, TimeUnit flowRateUnit) { 59 if (capcity < 0 || flowRate < 0) { 60 throw new IllegalArgumentException("capcity、flowRate必须大于0!"); 61 } 62 return new BucketLimit(capcity, flowRate, flowRateUnit); 63 } 64 65 //当前线程加入漏桶,返回false,表示漏桶已满;true:表示被漏桶限流成功,可以继续处理任务 66 public boolean acquire() { 67 Thread thread = Thread.currentThread(); 68 Node node = new Node(thread); 69 if (this.queue.offer(node)) { 70 LockSupport.park(); 71 return true; 72 } 73 return false; 74 } 75 76 //漏桶中存放的元素 77 class Node { 78 private Thread thread; 79 80 public Node(Thread thread) { 81 this.thread = thread; 82 } 83 } 84 } 85 86 public static void main(String[] args) { 87 BucketLimit bucketLimit = BucketLimit.build(10, 60, TimeUnit.MINUTES); 88 for (int i = 0; i < 15; i++) { 89 new Thread(() -> { 90 boolean acquire = bucketLimit.acquire(); 91 System.out.println(System.currentTimeMillis() + " " + acquire); 92 try { 93 TimeUnit.SECONDS.sleep(1); 94 } catch (InterruptedException e) { 95 e.printStackTrace(); 96 } 97 }).start(); 98 } 99 } 100 101 }
代码中BucketLimit.build(10, 60, TimeUnit.MINUTES);创建了一个容量为10,流水为60/分钟的漏桶。
代码中用到的技术有:
使用令牌桶算法来进行限流
令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。这种算法可以应对突发程度的请求,因此比漏桶算法好。
令牌桶算法示意图:

有兴趣的可以自己去实现一个。
限流工具类RateLimiter
Google开源工具包Guava提供了限流工具类RateLimiter,可以非常方便的控制系统每秒吞吐量,示例代码如下:
1 package com.itsoku.chat29; 2 3 import com.google.common.util.concurrent.RateLimiter; 4 5 import java.util.Calendar; 6 import java.util.Date; 7 import java.util.Objects; 8 import java.util.concurrent.ArrayBlockingQueue; 9 import java.util.concurrent.BlockingQueue; 10 import java.util.concurrent.TimeUnit; 11 import java.util.concurrent.atomic.AtomicInteger; 12 import java.util.concurrent.locks.LockSupport; 13 14 /** 15 * 跟着阿里p7学并发,微信公众号:javacode2018 16 */ 17 public class Demo3 { 18 19 public static void main(String[] args) throws InterruptedException { 20 RateLimiter rateLimiter = RateLimiter.create(5);//设置QPS为5 21 for (int i = 0; i < 10; i++) { 22 rateLimiter.acquire(); 23 System.out.println(System.currentTimeMillis()); 24 } 25 System.out.println("----------"); 26 //可以随时调整速率,我们将qps调整为10 27 rateLimiter.setRate(10); 28 for (int i = 0; i < 10; i++) { 29 rateLimiter.acquire(); 30 System.out.println(System.currentTimeMillis()); 31 } 32 } 33 }
输出:
1566284028725 1566284028922 1566284029121 1566284029322 1566284029522 1566284029721 1566284029921 1566284030122 1566284030322 1566284030522 ---------- 1566284030722 1566284030822 1566284030921 1566284031022 1566284031121 1566284031221 1566284031321 1566284031422 1566284031522 1566284031622
代码中RateLimiter.create(5)创建QPS为5的限流对象,后面又调用rateLimiter.setRate(10);将速率设为10,输出中分2段,第一段每次输出相隔200毫秒,第二段每次输出相隔100毫秒,可以非常精准的控制系统的QPS。
上面介绍的这些,业务中可能会用到,也可以用来应对面试。
浙公网安备 33010602011771号