20.线程系列- 高并发中常见的限流方式(并发控制,漏桶,令牌桶(RateLimiter))

本文内容

  1. 介绍常见的限流算法
  2. 通过控制最大并发数来进行限流
  3. 通过漏桶算法来进行限流
  4. 通过令牌桶算法来进行限流
  5. 限流工具类RateLimiter

常见的限流算法

  1. 通过控制最大并发数来进行限流
  2. 通过漏桶算法来进行限流
  3. 通过令牌桶算法来进行限流

通过控制最大并发数来进行限流

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

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

JUC中提供了这样的工具类,Semaphore。

public class Demo1 {

    static Semaphore semaphore = new Semaphore(5);

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                boolean flag = false;
                try {
                    flag = semaphore.tryAcquire(100, TimeUnit.MICROSECONDS);
                    if (flag) {
                        //休眠2秒,模拟下单操作
                        System.out.println(Thread.currentThread() + ",尝试下单中。。。。。");
                        TimeUnit.SECONDS.sleep(2);
                    } else {
                        System.out.println(Thread.currentThread() + ",秒杀失败,请稍微重试!");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (flag) {
                        semaphore.release();
                    }
                }
            }).start();
        }
    }

}

输出:

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],秒杀失败,请稍微重试!

使用漏桶算法来进行限流

国庆期间比较火爆的景点,人流量巨大,一般入口处会有限流的弯道,让游客进去进行排序,排在前面的人,每隔一段时间会放一波进入景区。排队人数超过了指定的限制,后面再来的人会被告知今天已经游客量达到峰值,会被拒绝排队,让其明天或者以后再来,这种玩法采用漏桶限流的方式。

漏桶算法思路很简单,水先进入到漏桶中,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

示例如下:

public class demo2 {

    static class BucketLimit{
        static AtomicInteger threadNum = new AtomicInteger(1);
        //容量
        private int capcity;
        //流速
        private int flowRate;
        //流速时间单位
        private TimeUnit flowRateUnit;
        //漏桶流出的任务时间间隔(纳秒)
        private long flowRateNanosTime;
        //队列
        private BlockingQueue<Node> queue;
        //漏桶中存放的元素
        class Node {
            private Thread thread;

            public Node(Thread thread) {
                this.thread = thread;
            }
        }

        public BucketLimit(int capcity, int flowRate, TimeUnit flowRateUnit) {
            this.capcity = capcity;
            this.flowRate = flowRate;
            this.flowRateUnit = flowRateUnit;
            this.bucketThreadWork();
        }
        //漏桶线程
        public void bucketThreadWork() {
            this.queue = new ArrayBlockingQueue<Node>(capcity);
            //漏桶流出的任务时间间隔(纳秒)
            this.flowRateNanosTime = flowRateUnit.toNanos(1) / flowRate;
            Thread thread = new Thread(this::bucketWork);
            thread.setName("漏桶线程-" + threadNum.getAndIncrement());
            thread.start();
        }
        //漏桶线程开始工作
        public void bucketWork() {
            while (true) {
                //从队列中移除node
                Node node = this.queue.poll();
                if (Objects.nonNull(node)) {
                    //唤醒任务线程
                    LockSupport.unpark(node.thread);
                }
                //休眠flowRateNanosTime
                LockSupport.parkNanos(this.flowRateNanosTime);
            }
        }
        //返回一个漏桶
        public static BucketLimit build(int capcity, int flowRate, TimeUnit flowRateUnit) {
            if (capcity < 0 || flowRate < 0) {
                throw new IllegalArgumentException("capcity、flowRate必须大于0!");
            }
            return new BucketLimit(capcity, flowRate, flowRateUnit);
        }
        //当前线程加入漏桶,返回false,表示漏桶已满;
        // true:表示被漏桶限流成功,可以继续处理任务
        public boolean acquire() {
            Thread thread = Thread.currentThread();
            Node node = new Node(thread);
            if (this.queue.offer(node)) {
                LockSupport.park();
                return true;
            }
            return false;
        }
    }
    public static void main(String[] args) {
        BucketLimit bucketLimit = BucketLimit.build(10, 60, TimeUnit.MINUTES);
        for (int i = 0; i < 15; i++) {
            new Thread(() -> {
                boolean acquire = bucketLimit.acquire();
                System.out.println(System.currentTimeMillis() + " " + acquire);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

输出:

1608538375003 false
1608538375003 false
1608538375003 false
1608538375003 false
1608538375003 false
1608538375999 true
1608538377000 true
1608538378000 true
1608538379000 true
1608538380001 true
1608538381001 true
1608538382001 true
1608538383001 true
1608538384002 true
1608538385002 true

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

使用令牌桶算法来进行限流

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

限流工具类RateLimiter

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

public class demo3 {

    public static void main(String[] args) throws InterruptedException {
        RateLimiter rateLimiter = RateLimiter.create(5);//设置QPS为5
        for (int i = 0; i < 10; i++) {
            rateLimiter.acquire();
            System.out.println(System.currentTimeMillis());
        }
        System.out.println("----------");
        //可以随时调整速率,我们将qps调整为10
        rateLimiter.setRate(10);
        for (int i = 0; i < 10; i++) {
            rateLimiter.acquire();
            System.out.println(System.currentTimeMillis());
        }
    }
}

输出:

1608539111331
1608539111539
1608539111730
1608539111931
1608539112130
1608539112331
1608539112530
1608539112731
1608539112930
1608539113130
----------
1608539113330
1608539113430
1608539113530
1608539113630
1608539113731
1608539113830
1608539113931
1608539114031
1608539114130
1608539114230
Disconnected from the target VM, address: '127.0.0.1:61858', transport: 'socket'

Process finished with exit code 0

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

上面介绍的这些,业务中可能会用到,也可以用来应对面试。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在高并发环境线程使用策略可以有很多种。常见的有: 1. 使用线程池,可以预先创建好一定数量的线程,减少线程创建和销毁的开销。 2. 使用队列,可以将请求放入队列线程从队列获取请求并处理。 3. 使用线程本地存储,可以为每个线程配置独立的存储空间,避免线程之间的数据竞争。 4. 使用锁机制,对共享资源加锁保护,避免多线程之间的数据不一致。 5. 使用信号量,对并发访问进行限制,保证系统的稳定性。 选择哪种策略需要根据具体场景和性能要求来判断。 ### 回答2: 在高并发线程的使用策略非常重要,可以有效提高系统的性能和吞吐量。 首先,需要合理设置线程池的大小。线程池的大小应根据系统的实际情况进行调整,在保证系统稳定性的前提下尽可能利用多核处理器的性能。如果线程池过小,无法处理大量的请求,导致大量请求被阻塞或拒绝;而如果线程池过大,则会造成资源的浪费和操作系统的负担,甚至导致系统崩溃。 其次,可以使用多线程技术进行并发处理。可以将任务分解为多个子任务,并利用多个线程同时执行这些子任务,以提高系统的响应速度和并发能力。通过合理地利用线程间的同步和通信机制,可以充分发挥多线程的优势。 另外,可以采用线程池的技术来管理和调度线程。线程池可以预先创建一定数量的线程,并管理它们的生命周期,以及接收和处理任务。通过线程池,可以避免频繁创建和销毁线程的开销,提高系统的性能。 还可以利用异步编程的方式来处理高并发。通过异步操作,可以让线程不必等待某些阻塞的操作返回结果,而是可以立即进行其他任务的处理。这样可以提高系统的性能和吞吐量。 最后,可以通过采用并发安全的数据结构和算法来处理高并发并发安全的数据结构和算法可以保证多个线程同时访问时的数据一致性和可靠性,避免出现竞态条件等问题。 综上所述,线程的使用策略在高并发非常重要。通过合理设置线程池的大小、利用多线程技术、使用线程池、采用异步编程和并发安全的数据结构等方法,可以提高系统的并发能力和性能。 ### 回答3: 在高并发的场景线程使用策略非常重要。以下是一些常见线程使用策略: 1. 线程池:使用线程池可以有效地管理线程资源。通过预先创建一定数量的线程,可以避免频繁创建和销毁线程的开销。线程池使用一个队列来存储任务,线程从队列获取任务并执行。线程执行完任务后,会返回线程池,准备接受下一个任务。 2. 分段锁:在高并发环境下,使用全局锁可能会导致线程争用同一资源,降低并发性能。为了减少锁的粒度,可以使用分段锁。将资源划分为多个部分,每个部分有一个对应的锁,这样在访问资源时,只需要锁定对应的部分,降低了线程之间的竞争。 3. 异步编程:在高并发场景下,使用异步编程可以提高系统的吞吐量。将部分任务转换为异步进行,可以避免线程等待的情况,提高资源的利用率。异步编程可以通过多线程、事件驱动或协程等方式来实现。 4. 非阻塞IO:在高并发,IO操作往往会成为性能瓶颈。使用非阻塞IO可以实现在一个线程内同时处理多个IO操作,减少线程切换开销。非阻塞IO通常使用事件驱动的方式,当IO操作完成时,触发相应的事件进行处理。 5. 并发容器和并发算法:在高并发环境下,使用线程安全的容器和算法可以减少线程之间的竞争。例如,使用ConcurrentHashMap替代HashMap,使用ConcurrentLinkedQueue替代LinkedList等。 综上所述,线程使用策略在高并发是非常重要的。通过合理的线程池管理、分段锁、异步编程、非阻塞IO以及使用线程安全的容器和算法,可以提高系统的并发性能和吞吐量,提升用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值