java程序员们别再傻傻的使用单线程同步处理了,学会使用消息队列,让你的系统快到飞起来

需求描述大多数电商系统大促期间都会遭遇了高并发访问,导致系统崩溃,影响了用户购物体验和平台的声誉。为了避免类似情况再次发生,决定进行Java高并发调优技术部商讨后的调优方向如下:

1. 优化数据库连接池:通过调整连接池的大小和超时时间,减少数据库连接的等待时间和占用资源,提高系统的并发处理能力。

2. 使用缓存技术:将热点数据缓存到内存中,减少数据库的访问次数,提高系统的响应速度和并发处理能力。

3. 使用分布式缓存:将缓存数据分布到多个节点上,提高系统的可扩展性和容错性。

4. 使用消息队列:将请求异步化,通过消息队列进行处理,减少系统的响应时间和并发处理能力。

5. 使用分布式锁:通过分布式锁控制并发访问,避免数据的冲突和重复处理。

6. 使用负载均衡:通过负载均衡将请求分发到多个节点上,提高系统的并发处理能力和可用性。

7.    使用异步处理:通过异步处理将请求分发到多个线程上,提高系统的并发处理能力和响应速度。

本文针对方案4使用消息队列 进行重点分析和系统调优,并对调优前后的效果做对比分析,给广大猿友们提供一个技术参考,能够切实地运用到自己的系统中,

其他方案将会在后续文章里给大家做分享,敬请期待

  • 首先要知道的两个名词

同步和异步

同步是指,同步请求,就是按顺序处理,即当我们向服务器发出一个请求时,在服务器没返回结果给客户端之前,我们要一直处于等待状态直至服务器将结果返回到客户端,我们才能执行下一步操作。例如我们经常使用浏览器去访问一个网站的时候,其实就是同步请求,也就是浏览器发出一个请求,服务器就回复一个请求

异步指的就是异步请求,也就是java上说的并行处理。即当我们向服务器发出一个请求时,在服务器没返回结果之前,我们还是可以执行其他操作。

举一个简单的例子,泡茶。泡茶需要的步骤有烧水,洗杯子,装茶叶,倒水。同步的话,就是在烧水的时候就等着,直到水烧开后,再去洗杯子,洗完杯子后再去装茶叶,最后再倒水。而异步的话就是指在烧水的时候我们不用一直等着,我们可以先去做后面的几件事。

这个例子其实不是很恰当,因为不过是同步还是异步实际在程序运行上所花费的时间是一样的,但是对于用户而言,异步花费的时间更少。

简而言之

同步,是所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。

异步,不用等所有操作等做完,就相应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。

优缺点对比

同步

  优点:一步一步完成,确保了每一步的正确性,不容易出错

  缺点:用户的等待时间会花费的更多,体验不好

异步

  优点:用户的等待时间会花费的更少,体验更好。

  缺点:多个请求并行处理完成,减少了用户等待时间,但是最后容易出错,  

     且不易发现错误。

  • 使用消息队列的好处

消息队列提供了可靠的消息传递机制,可以在分布式系统中实现异步通信、解耦系统组件、削峰填谷等功能。选择适合的消息队列取决于应用程序的需求和场景,例如吞吐量要求、消息持久化需求、可靠性要求等。

  • 常用的几个消息队列

Apache Kafka:一个分布式流平台,具有高吞吐量、可持久化、可扩展等特点,广泛用于构建实时数据流和事件驱动的应用程序。

RabbitMQ:一个开源的消息代理,实现了高级消息队列协议(AMQP),支持多种消息传递模式和高度可定制的消息路由规则。

ActiveMQ:一个开源的消息中间件,支持Java Message Service(JMS)规范,具有高性能、可靠性和可扩展性。

RocketMQ:一个分布式消息中间件,具有低延迟、高吞吐量、分布式特性和可靠性等特点,适用于大规模分布式系统的消息通信。

Redis:一个内存数据存储系统,也可以用作消息队列,通过发布/订阅模式和列表数据结构实现简单的消息传递。

总结:以上队列都是基于外置中间件来使用的,优点是开源使用,吞吐量较高,可与java无缝对接,缺点是需要下载安装单独的中间件,在服务器资源紧张的今天,多购买一台服务器的都是在烧钱,本文所分享的是基于 java内置的消息队列,省去额外购买服务器的麻烦,还能轻松拿捏你的代码,让你丝滑调优你的应用程序,

  • 基于java的内置消息队列

从不同的维度进行分类

 

  1. 按阻塞分类

阻塞队列(Blocking Queue)提供了可阻塞的 put 和 take 方法,它们与可定时的 offer 和 poll 是等价的。如果队列满了 put 方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么 take 方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put 方法和 take 方法就永远不会阻塞。

我们可以从队列的名称中知道此队列是否为阻塞队列,阻塞队列中包含 BlockingQueue 关键字,比如以下这些:

  1.   ArrayBlockingQueue
  2.   LinkedBlockingQueue
  3.   PriorityBlockingQueue

阻塞队列功能演示

import java.util.Date;

import java.util.concurrent.ArrayBlockingQueue;



public class BlockingTest {

    public static void main(String[] args) throws InterruptedException {

        // 创建一个长度为 5 的阻塞队列

        ArrayBlockingQueue q1 = new ArrayBlockingQueue(5);

        

        // 新创建一个线程执行入列

        new Thread(() -> {

            // 循环 10 次

            for (int i = 0; i < 10; i++) {

                try {

                    q1.put(i);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println(new Date() + " | ArrayBlockingQueue Size:" + q1.size());

            }

            System.out.println(new Date() + " | For End.");

        }).start();



        // 新创建一个线程执行出列

        new Thread(() -> {

            for (int i = 0; i < 5; i++) {

                try {

                    // 休眠 1S

                    Thread.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                if (!q1.isEmpty()) {

                    try {

                        q1.take(); // 出列

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                }

            }

        }).start();

    }

}

以上代码的执行结果如下:

Mon Oct 19 20:16:12 CST 2022 | ArrayBlockingQueue Size:1

Mon Oct 19 20:16:12 CST 2022 | ArrayBlockingQueue Size:2

Mon Oct 19 20:16:12 CST 2022 | ArrayBlockingQueue Size:3

Mon Oct 19 20:16:12 CST 2020 | ArrayBlockingQueue Size:4

Mon Oct 19 20:16:12 CST 2022 | ArrayBlockingQueue Size:5

Mon Oct 19 20:16:13 CST 2022 | ArrayBlockingQueue Size:5

Mon Oct 19 20:16:14 CST 2022 | ArrayBlockingQueue Size:5

Mon Oct 19 20:16:15 CST 2022 | ArrayBlockingQueue Size:5

Mon Oct 19 20:16:16 CST 2022 | ArrayBlockingQueue Size:5

Mon Oct 19 20:16:17 CST 2022 | ArrayBlockingQueue Size:5

Mon Oct 19 20:16:17 CST 2022 | For End.

从上述结果可以看出,当 ArrayBlockingQueue 队列满了之后就会进入阻塞,当过了 1 秒有元素从队列中移除之后,才会将新的元素入列。

阻塞队列非阻塞队列也就是普通队列,它的名字中不会包含 BlockingQueue 关键字,并且它不会包含 put 和 take 方法,当队列满之后如果还有新元素入列会直接返回错误,并不会阻塞的等待着添加元素,如下图所示

 

非阻塞队列的典型代表是 ConcurrentLinkedQueue 和 PriorityQueue

  1. 按大小分类

有界队列:是指有固定大小的队列,比如设定了固定大小的 ArrayBlockingQueue,又或者大小为 0 的 SynchronousQueue。

 

无界队列:指的是没有设置固定大小的队列,但其实如果没有设置固定大小也是有默认值的,只不过默认值是 Integer.MAX_VALUE,当然实际的使用中不会有这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的角度来看相当于 “无界”的。

 

  1. 按功能分类

  1. 普通队列:普通队列(Queue)是指实现了先进先出的基本队列,例如 ArrayBlockingQueue 和 LinkedBlockingQueue,其中 ArrayBlockingQueue 是用数组实现的普通队列,如下图所示:

 

 LinkedBlockingQueue 是使用链表实现的普通队列,如下图所示:

 

常用方法

普通队列中的常用方法有以下这些:

  • offer():添加元素,如果队列已满直接返回 false,队列未满则直接插入并返回 true;
  • poll():删除并返回队头元素,当队列为空返回 null;
  • add():添加元素,此方法是对 offer 方法的简单封装,如果队列已满,抛出 IllegalStateException 异常;
  • remove():直接删除队头元素;
  • put():添加元素,如果队列已经满,则会阻塞等待插入;
  • take():删除并返回队头元素,当队列为空,则会阻塞等待;
  • peek():查询队头元素,但不会进行删除;
  • element():对 peek 方法进行简单封装,如果队头元素存在则取出并不删除,如果不存在抛出 NoSuchElementException 异常。

注意:一般情况下 offer() 和 poll() 方法配合使用,put() 和 take() 阻塞方法配合使用,add() 和 remove() 方法会配合使用,程序中常用的是 offer() 和 poll() 方法,因此这两个方法比较友好,不会报错

 LinkedBlockingQueue 为例,演示一下普通队列的使用:

import java.util.concurrent.LinkedBlockingQueue;

static class LinkedBlockingQueueTest {

    public static void main(String[] args) {

        LinkedBlockingQueue queue = new LinkedBlockingQueue();

        queue.offer("Hello");

        queue.offer("Java");

        queue.offer("Jason");

        while (!queue.isEmpty()) {

            System.out.println(queue.poll());

        }

    }

}

以上代码的执行结果如下:

Hello

Java

Jason
  1. 双端队列:

双端队列(Deque)是指队列的头部和尾部都可以同时入队和出队的数据结构,如下图所示:

 

双端队列 LinkedBlockingDeque 的使用:

import java.util.concurrent.LinkedBlockingDeque;
/**

  * 双端队列示例

  */

static class LinkedBlockingDequeTest {

    public static void main(String[] args) {

        // 创建一个双端队列

        LinkedBlockingDeque deque = new LinkedBlockingDeque();

        deque.offer("offer"); // 插入首个元素

        deque.offerFirst("offerFirst"); // 队头插入元素

        deque.offerLast("offerLast"); // 队尾插入元素

        while (!deque.isEmpty()) {

            // 从头遍历打印

            System.out.println(deque.poll());

        }

    }

}

以上代码的执行结果如下:

offerFirst

offer

offerLast
  1. 优先队列:

优先队列(PriorityQueue)是一种特殊的队列,它并不是先进先出的,而是优先级高的元素先出队。

优先队列是根据二叉堆实现的,二叉堆的数据结构如下图所示:

 

二叉堆分为两种类型:一种是最大堆一种是最小堆。以上展示的是最大堆,在最大堆中,任意一个父节点的值都大于等于它左右子节点的值。

因为优先队列是基于二叉堆实现的,因此它可以将优先级最高的元素先出队。

优先队列的使用:

import java.util.PriorityQueue;

public class PriorityQueueTest {

    // 自定义的实体类

    static class Viper {

        private int id; // id

        private String name; // 名称

        private int level; // 等级



        public Viper(int id, String name, int level) {

            this.id = id;

            this.name = name;

            this.level = level;

        }



        public int getId() {

            return id;

        }



        public void setId(int id) {

            this.id = id;

        }



        public String getName() {

            return name;

        }



        public void setName(String name) {

            this.name = name;

        }



        public int getLevel() {

            return level;

        }



        public void setLevel(int level) {

            this.level = level;

        }

    }

    public static void main(String[] args) {

  PriorityQueue queue = new PriorityQueue(10, new Comparator<Viper>() {

            @Override

            public int compare(Viper v1, Viper v2) {

                // 设置优先级规则(倒序,等级越高权限越大)

                return v2.getLevel() - v1.getLevel();

            }

        });

        // 构建实体类

        Viper v1 = new Viper(1, "Java", 1);

        Viper v2 = new Viper(2, "MySQL", 5);

        Viper v3 = new Viper(3, "Redis", 3);

        // 入列

        queue.offer(v1);

        queue.offer(v2);

        queue.offer(v3);

        while (!queue.isEmpty()) {

            // 遍历名称

            Viper item = (Viper) queue.poll();

            System.out.println("Name:" + item.getName() +

                               " Level:" + item.getLevel());

        }

    }

}

以上代码的执行结果如下:

Name:MySQL Level:5

Name:Redis Level:3

Name:Java Level:1

从上述结果可以看出,优先队列的出队是不考虑入队顺序的,它始终遵循的是优先级高的元素先出队

  1. 延迟队列:

延迟队列(DelayQueue)是基于优先队列 PriorityQueue 实现的,它可以看作是一种以时间为度量单位的优先的队列,当入队的元素到达指定的延迟时间之后方可出队。

 

延迟队列的使用:

import lombok.Getter;

import lombok.Setter;

import java.text.DateFormat;

import java.util.Date;

import java.util.concurrent.DelayQueue;

import java.util.concurrent.Delayed;

import java.util.concurrent.TimeUnit;



public class CustomDelayQueue {

    // 延迟消息队列

    private static DelayQueue delayQueue = new DelayQueue();

    public static void main(String[] args) throws InterruptedException {

        producer(); // 调用生产者

        consumer(); // 调用消费者

    }



    // 生产者

    public static void producer() {

        // 添加消息

        delayQueue.put(new MyDelay(1000, "消息1"));

        delayQueue.put(new MyDelay(3000, "消息2"));

    }



    // 消费者

    public static void consumer() throws InterruptedException {

        System.out.println("开始执行时间:" +

                DateFormat.getDateTimeInstance().format(new Date()));

        while (!delayQueue.isEmpty()) {

            System.out.println(delayQueue.take());

        }

        System.out.println("结束执行时间:" +

                DateFormat.getDateTimeInstance().format(new Date()));

    }



    static class MyDelay implements Delayed {

        // 延迟截止时间(单位:毫秒)

        long delayTime = System.currentTimeMillis();

        // 借助 lombok 实现

        @Getter

        @Setter

        private String msg;



        /**

         * 初始化

         * @param delayTime 设置延迟执行时间

         * @param msg       执行的消息

         */

        public MyDelay(long delayTime, String msg) {

            this.delayTime = (this.delayTime + delayTime);

            this.msg = msg;

        }



        // 获取剩余时间

        @Override

        public long getDelay(TimeUnit unit) {

            return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);

        }



        // 队列里元素的排序依据

        @Override

        public int compareTo(Delayed o) {

            if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {

                return 1;

            } else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {

                return -1;

            } else {

                return 0;

            }

        }

        @Override

        public String toString() {

            return this.msg;

        }

    }

}

以上代码的执行结果如下:

开始执行时间:2022-10-20 20:17:28

消息1

消息2

结束执行时间:2022-10-20 20:17:31

从上述结束执行时间和开始执行时间可以看出,消息 1 和消息 2 都正常实现了延迟执行的功能。

  1. 其他队列:

在 Java 的队列中有一个比较特殊的队列 SynchronousQueue,它的特别之处在于它内部没有容器,每次进行 put() 数据后(添加数据),必须等待另一个线程拿走数据后才可以再次添加数据,它的使用示例如下:

import java.util.concurrent.SynchronousQueue;

public class SynchronousQueueTest {



    public static void main(String[] args) {

        SynchronousQueue queue = new SynchronousQueue();



        // 入队

        new Thread(() -> {

            for (int i = 0; i < 3; i++) {

                try {

                    System.out.println(new Date() + ",元素入队");

                    queue.put("Data " + i);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }



            }

        }).start();



        // 出队

        new Thread(() -> {

            while (true) {

                try {

                    Thread.sleep(1000);

                    System.out.println(new Date() + ",元素出队:" + queue.take());

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }).start();

    }

}

以上代码的执行结果如下:

Mon Oct 19 21:00:21 CST 2022,元素入队

Mon Oct 19 21:00:22 CST 2022,元素出队:Data 0

Mon Oct 19 21:00:22 CST 2022,元素入队

Mon Oct 19 21:00:23 CST 2022,元素出队:Data 1

Mon Oct 19 21:00:23 CST 2022,元素入队

Mon Oct 19 21:00:24 CST 2022,元素出队:Data 2

从上述结果可以看出,当有一个元素入队之后,只有等到另一个线程将元素出队之后,新的元素才能再次入队。

总结:

Java高并发调优是一个复杂的过程,需要综合考虑多个因素,包括硬件、软件、网络等方面。通过优化数据库连接池、使用缓存技术、分布式缓存、消息队列、分布式锁、负载均衡和异步处理等技术手段,可以提高系统的并发处理能力和响应速度,避免系统崩溃和影响用户购物体验。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值