常见的限流的场景

  1. 秒杀活动,数量有限,访问量巨大,为了防止系统宕机,需要做限流处理。
  2. 国庆期间,一般的旅游景点人口太多,采用排队方式做限流处理。
  3. 医院看病通过发放排队号的方式来做限流处理。

    常见的限流算法

    1、控制最大并发数

    以秒杀业务为例,10个iphone,100万人抢购,100万人同时发起请求,最终能够抢到的人也就是前面几个人,后面的基本上都没有希望了,那么我们可以通过控制并发数来实现,比如并发数控制在10个,其他超过并发数的请求全部拒绝,提示:秒杀失败,请稍后重试。

控制最大并发数来进行限流,通俗解释:一大波人去商场购物,必须经过一个门口,门口有个门卫,兜里面有指定数量的门禁卡,来的人先去门卫那边拿取门禁卡,拿到卡的人才可以刷卡进入商场,拿不到的可以继续等待。进去的人出来之后会把卡归还给门卫,门卫可以把归还来的卡继续发放给其他排队的顾客使用。

JUC中提供了这样的工具类:Semaphore,示例代码:

  1. public class Demo1 {
  2. static Semaphore semaphore = new Semaphore(5);
  3. public static void main(String[] args) {
  4. for (int i = 0; i < 20; i++) {
  5. new Thread(() -> {
  6. boolean flag = false;
  7. try {
  8. flag = semaphore.tryAcquire(100, TimeUnit.MICROSECONDS);
  9. if (flag) {
  10. //休眠2秒,模拟下单操作
  11. System.out.println(Thread.currentThread() + ",尝试下单中。。。。。");
  12. TimeUnit.SECONDS.sleep(2);
  13. } else {
  14. System.out.println(Thread.currentThread() + ",秒杀失败,请稍微重试!");
  15. }
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. } finally {
  19. if (flag) {
  20. semaphore.release();
  21. }
  22. }
  23. }).start();
  24. }
  25. }
  26. }

输出:

  1. Thread[Thread-10,5,main],尝试下单中。。。。。
  2. Thread[Thread-8,5,main],尝试下单中。。。。。
  3. Thread[Thread-9,5,main],尝试下单中。。。。。
  4. Thread[Thread-12,5,main],尝试下单中。。。。。
  5. Thread[Thread-11,5,main],尝试下单中。。。。。
  6. Thread[Thread-2,5,main],秒杀失败,请稍微重试!
  7. Thread[Thread-1,5,main],秒杀失败,请稍微重试!
  8. Thread[Thread-18,5,main],秒杀失败,请稍微重试!
  9. Thread[Thread-16,5,main],秒杀失败,请稍微重试!
  10. Thread[Thread-0,5,main],秒杀失败,请稍微重试!
  11. Thread[Thread-3,5,main],秒杀失败,请稍微重试!
  12. Thread[Thread-14,5,main],秒杀失败,请稍微重试!
  13. Thread[Thread-6,5,main],秒杀失败,请稍微重试!
  14. Thread[Thread-13,5,main],秒杀失败,请稍微重试!
  15. Thread[Thread-17,5,main],秒杀失败,请稍微重试!
  16. Thread[Thread-7,5,main],秒杀失败,请稍微重试!
  17. Thread[Thread-19,5,main],秒杀失败,请稍微重试!
  18. Thread[Thread-15,5,main],秒杀失败,请稍微重试!
  19. Thread[Thread-4,5,main],秒杀失败,请稍微重试!
  20. Thread[Thread-5,5,main],秒杀失败,请稍微重试!

关于Semaphore的使用,可以移步:JUC中的Semaphore(信号量)

2、漏桶算法

国庆期间比较火爆的景点,人流量巨大,一般入口处会有限流的弯道,让游客进去进行排队,排在前面的人,每隔一段时间会放一拨进入景区。排队人数超过了指定的限制,后面再来的人会被告知今天已经游客量已经达到峰值,会被拒绝排队,让其明天或者以后再来,这种玩法采用漏桶限流的方式。
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,以任意速率流入水,当水超过桶容量则丢弃,因为桶容量是不变的,保证了整体的速率,所以可以保证接口会以一个常速速率来处理请求。

漏桶算法示意图:
高并发中常见的限流方式 - 图1

简陋版的实现,代码如下:

  1. public class Demo2 {
  2. public static class BucketLimit {
  3. static AtomicInteger threadNum = new AtomicInteger(1);
  4. //容量
  5. private int capcity;
  6. //流速
  7. private int flowRate;
  8. //流速时间单位
  9. private TimeUnit flowRateUnit;
  10. private BlockingQueue<Node> queue;
  11. //漏桶流出的任务时间间隔(纳秒)
  12. private long flowRateNanosTime;
  13. public BucketLimit(int capcity, int flowRate, TimeUnit flowRateUnit) {
  14. this.capcity = capcity;
  15. this.flowRate = flowRate;
  16. this.flowRateUnit = flowRateUnit;
  17. this.bucketThreadWork();
  18. }
  19. //漏桶线程
  20. public void bucketThreadWork() {
  21. this.queue = new ArrayBlockingQueue<Node>(capcity);
  22. //漏桶流出的任务时间间隔(纳秒)
  23. this.flowRateNanosTime = flowRateUnit.toNanos(1) / flowRate;
  24. Thread thread = new Thread(this::bucketWork);
  25. thread.setName("漏桶线程-" + threadNum.getAndIncrement());
  26. thread.start();
  27. }
  28. //漏桶线程开始工作
  29. public void bucketWork() {
  30. while (true) {
  31. Node node = this.queue.poll();
  32. if (Objects.nonNull(node)) {
  33. //唤醒任务线程
  34. LockSupport.unpark(node.thread);
  35. }
  36. //休眠flowRateNanosTime
  37. LockSupport.parkNanos(this.flowRateNanosTime);
  38. }
  39. }
  40. //返回一个漏桶
  41. public static BucketLimit build(int capcity, int flowRate, TimeUnit flowRateUnit) {
  42. if (capcity < 0 || flowRate < 0) {
  43. throw new IllegalArgumentException("capcity、flowRate必须大于0!");
  44. }
  45. return new BucketLimit(capcity, flowRate, flowRateUnit);
  46. }
  47. //当前线程加入漏桶,返回false,表示漏桶已满;true:表示被漏桶限流成功,可以继续处理任务
  48. public boolean acquire() {
  49. Thread thread = Thread.currentThread();
  50. Node node = new Node(thread);
  51. if (this.queue.offer(node)) {
  52. LockSupport.park();
  53. return true;
  54. }
  55. return false;
  56. }
  57. //漏桶中存放的元素
  58. class Node {
  59. private Thread thread;
  60. public Node(Thread thread) {
  61. this.thread = thread;
  62. }
  63. }
  64. }
  65. public static void main(String[] args) {
  66. BucketLimit bucketLimit = BucketLimit.build(10, 60, TimeUnit.MINUTES);
  67. for (int i = 0; i < 15; i++) {
  68. new Thread(() -> {
  69. boolean acquire = bucketLimit.acquire();
  70. System.out.println(System.currentTimeMillis() + " " + acquire);
  71. try {
  72. TimeUnit.SECONDS.sleep(1);
  73. } catch (InterruptedException e) {
  74. e.printStackTrace();
  75. }
  76. }).start();
  77. }
  78. }
  79. }

代码中BucketLimit.build(10, 60, TimeUnit.MINUTES)创建了一个容量为10,流水为60/分钟的漏桶。
代码中用到的技术有:

  1. BlockingQueue阻塞队列
  2. JUC中的LockSupport工具类,必备技能

3、令牌桶算法

令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。这种算法可以应对突发程度的请求,因此比漏桶算法好。

令牌桶算法示意图:
高并发中常见的限流方式 - 图2

限流工具类RateLimiter

Google开源工具包Guava提供了限流工具类RateLimiter,可以非常方便的控制系统每秒吞吐量,示例代码如下:

  1. public class Demo3 {
  2. public static void main(String[] args) throws InterruptedException {
  3. RateLimiter rateLimiter = RateLimiter.create(5);//设置QPS为5
  4. for (int i = 0; i < 10; i++) {
  5. rateLimiter.acquire();
  6. System.out.println(System.currentTimeMillis());
  7. }
  8. System.out.println("----------");
  9. //可以随时调整速率,我们将qps调整为10
  10. rateLimiter.setRate(10);
  11. for (int i = 0; i < 10; i++) {
  12. rateLimiter.acquire();
  13. System.out.println(System.currentTimeMillis());
  14. }
  15. }
  16. }

输出:

  1. 1566284028725
  2. 1566284028922
  3. 1566284029121
  4. 1566284029322
  5. 1566284029522
  6. 1566284029721
  7. 1566284029921
  8. 1566284030122
  9. 1566284030322
  10. 1566284030522
  11. ----------
  12. 1566284030722
  13. 1566284030822
  14. 1566284030921
  15. 1566284031022
  16. 1566284031121
  17. 1566284031221
  18. 1566284031321
  19. 1566284031422
  20. 1566284031522
  21. 1566284031622

代码中RateLimiter.create(5)创建QPS为5的限流对象,后面又调用rateLimiter.setRate(10)将速率设为10,输出中分2段,第一段每次输出相隔200毫秒,第二段每次输出相隔100毫秒,可以非常精准的控制系统的QPS。

令牌桶算法VS漏桶算法

漏桶
漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。
令牌桶
生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。