消息队列---RabbitMQ的学习(三)

 书上有路勤为径,学海无涯苦作舟。

自述

   RabbitMQ的第二个场景------工作队列/任务队列。
在这里插入图片描述

工作队列

   工作队列(又名:任务队列)背后主要思想是避免立即执行资源密集型任务而不得不等待它完成,相反,我们安排任务稍后完成,将任务封装为消息并将其发送到队列中,在后台运行的工作进程将获取到该任务并执行该任务,当有多个工作者时,任务将在它们之间共享,简单理解:工作队列实现了在消费者之间分配任务。
                         在这里插入图片描述

工作队列模型分解

   生产者将任务封装为消息发送到队列中,而消费者监听到队列中的消息就会进行获取,获取到消息之后将进行处理,工作队列和简单列队的区别在于:简单队列是一对一,而工作队列是一对多,在工作队列场景下,消费者是存在资源抢占的,处理数据较快的消费者,获取的数据自然多些,而处理数据慢的消费者,获取的数据也就少些。
   ps:自己画的图,美观可能不太好,望大佬将就看。
ps:自己画的图,勿喷~~

轮询分发

   RabbitMQ的工作队列默认分发是轮询分发 ,RabbitMQ 按顺序将每条消息发送给下一个消费者。平均而言,每个消费者将获得相同数量的消息,天上飞的理念,地上跑的实现,再多的理念也不如几行代码实在实用,好了,开始进入代码环节。

轮询分发:均匀的分摊给每一个消费者。
轮询分发

 Send.java

/**
 * 工作队列(任务队列)---生产者
 * ①工作任务:背后思想避免立即执行密集资源型任务,并不得不等待它完成
 * ②工作任务:用于在多个工作者/消费者之间分配耗时任务
 */
public class Send {
    private static final String QUEUE_NAME = "yunkai_queue"; //队列名称

    public static void main(String[] args) {
        try {
            //连接
            Connection connection = ConnectionUtil.getConnection();
            //获取核心管道/通道
            Channel channel = connection.createChannel();
            //创建队列
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            //正常发送信息
            String mgs = "vritual msg";
            for (int i = 0; i < 30; i++) {
                //发布
                channel.basicPublish("", QUEUE_NAME, null, (mgs+i).getBytes());
                //模拟资源紧张
                Thread.sleep(500);
            }
            System.out.println("send msg success");
            //关闭连接
            ConnectionUtil.closeConnection(connection,channel);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 Recv.java

/**
 * 工作队列(任务队列)---消费者
 * 该Recv类进行通用,启动两次,消费者一sleep时间间隔为1秒,而消费者二的sleep时间间隔为2秒。
 */
public class Recv {
    private static final String QUEUE_NAME = "yunkai_queue"; //队列名称

    public static void main(String[] args) {
        try {
            //连接
            Connection connection = ConnectionUtil.getConnection();
            //获取管道
            Channel channel = connection.createChannel();
            //创建队列--生产者或消费者不存在优先级顺序,生产者发送消息,消费者监听信息并获取信息
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            //模拟真实场景,消费者接收到数据,并使用数据的时候会有一定的延迟--DeliverCallback实时缓冲数据,并告诉队列将信息以异步形式传递给我们
            DeliverCallback callback = new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    String msg = new String(delivery.getBody());
                    System.out.println("接收的消息========" + msg);
                    try {
                        //模仿处理任务所耗时长
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            //消费
            channel.basicConsume(QUEUE_NAME, callback, consuer -> {
            });
        } catch (
                Exception e) {
            e.printStackTrace();
        }
    }
}

 额外补充
   单个类重复执行的设置方法,如果不设置的话,每次执行类的时候,类会重新启动。
在这里插入图片描述
   万事具备,只欠东风了,现在可以运行了,生产者或消费者先执行那个都可以,此场景下它们之间不存在优先级关系。
 测试效果:①消费者1:延迟一秒。②消费者2:延迟两秒。③生产者生产30条数据。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
 测试总结: 消费者1处理任务快,但执行的任务数量却和消费者2一致,这就是RabbitMQ默认情况下的轮询分发,如果队列中的数据总数是奇数的话,那么最后一条数据会发给最先获取数据的消费者(亲测~~)。

消息确认

   正常情况下,RabbitMQ将数据传递给消费者,同时也会将这条数据标记为删除,然后消费者拿到数据进行处理,但如果是非正常情况,例如:RabbitMQ将信息传递给消费者,但传递过程中,消费者挂掉或者连接丢失了,这就会导致这条数据丢失,并且无法还原,针对这一点我们可以将autoAck=true 的标识关闭,将它改为false,这样RabbitMQ在传递消息给消费者时不会将该消息标记为删除,而是等待着消费者接收到消息之后的ack响应(消息确认),RabbitMQ成功接收到消费者的ack响应之后才会将该数据标记为删除,而不是消息一传递出去就标记删除。
 Recv.java

/**
 * 工作队列(任务队列)---消费者
 * 该Recv类进行通用,启动两次,消费者一sleep时间间隔为1秒,而消费者二的sleep时间间隔为2秒。
 */
public class Recv {
    private static final String QUEUE_NAME = "yunkai_queue"; //队列名称

    public static void main(String[] args) {
        try {
            //连接
            Connection connection = ConnectionUtil.getConnection();
            //获取管道
            final Channel channel = connection.createChannel();
            //创建队列--生产者或消费者不存在优先级顺序,生产者发送消息,消费者监听信息并获取信息
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            //模拟真实场景,消费者接收到数据,并使用数据的时候会有一定的延迟--DeliverCallback实时缓冲数据,并告诉队列将信息以异步形式传递给我们
            DeliverCallback callback = new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    String msg = new String(delivery.getBody());
                    System.out.println("接收的消息========" + msg);
                    try {
                        //模仿处理任务所耗时长
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //新增finally块
                    finally {
                        //消息获取成功后ack响应
                        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    }
                }
            };
            //消费
            boolean autoAck = false; //关闭
            channel.basicConsume(QUEUE_NAME, autoAck, callback, consuer -> {
            });
            System.out.println("消费者1启动成功");
        } catch (
                Exception e) {
            e.printStackTrace();
        }
    }
}

消息持久性

   消息确认解决了消费者即使没有拿到数据,也可以保证数据不丢失的隐患,但如果RabbitMQ在使用的中途中挂掉了、服务器崩溃了、网络异常或RabbitMQ重启了等这些情况,还是会使数据丢失,所以我们在通过生产者发送信息的给RabbitMQ的时候就要告知RabbitMQ该系列数据是需要标记持久状态的。

注意一:不能在原有的队列中进行消息持久性设置,否则会报错,解决方法:①删除原有的队列。②重新创建一个不存在的队列。
注意二:此场景下生产者创建队列,消费者就无需创建队列了。

 Send.java

/**
 * 工作队列(任务队列)---生产者
 * ①工作任务:背后思想避免立即执行密集资源型任务,并不得不等待它完成
 * ②工作任务:用于在多个工作者/消费者之间分配耗时任务
 */
public class Send {
    private static final String QUEUE_NAME = "yunkai_queue"; //队列名称

    public static void main(String[] args) {
        try {
            //连接
            Connection connection = ConnectionUtil.getConnection();
            //获取核心管道/通道
            Channel channel = connection.createChannel();
            //创建队列
            boolean durable = true;//持久性开启--解决RabbitMQ重启不会丢失数据
            channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
            //正常发送信息
            String mgs = "vritual msg";
            for (int i = 0; i < 3; i++) {
                //MessageProperties.PERSISTENT_TEXT_PLAIN---解决了数据持久化,类似zk存储节点的持久化设置
                //发布
                channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, (mgs + i).getBytes());
            }
            System.out.println("send msg success");
            //关闭连接
            ConnectionUtil.closeConnection(connection, channel);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

官方的原话:消息标记为持久性并不能一定保证数据的持久性,虽然它告诉RabbitMQ将消息存储到磁盘上,但当RabbitMQ已经接收到一条消息却还没有保存的时候,仍然有丢失的可能性,RabbitMQ不会对每条消息都执行fsync(2)—它可能只是保存到缓存中,而不是真正的写到磁盘中。


fsync(2):把内存中的数据存储到磁盘中。

公平调度

   轮询分发的弊端:处理消息快的工作者会很闲,而处理消息慢的工作者会很忙,这种情况RabbitMQ自身是不知道的,它只知道要将信息均匀的分摊,但是并不是所有场景都适合轮询分发,为了解决此问题,我们可以使用prefetchCount = 1设置basicQos()方法,告诉RabbitMQ一次不要给一个消费者发多个消息,而是当消费者处理完上一条消息之后,在传递下一条消息,这种模式的话完美的解决了轮询分发的弊端,并且会出现多个消费者之间抢占资源情况,哪个消费者处理数据快,哪个消费者就会接收到的消息多。
                         
在这里插入图片描述
在这里插入图片描述

   生产者先启动,随后在启动消费者,因为设置成了消息持久化,只能一个程序创建队列,多个程序创建同一个队列的话会报错。
 Recv.java

**
 * 工作队列(任务队列)---消费者
 *Recv类进行通用,启动两次,消费者一sleep时间间隔为1,而消费者二的sleep时间间隔为2秒。
 */
public class Recv {
    private static final String QUEUE_NAME = "yunkai_queue"; //队列名称
    public static void main(String[] args) {
        try {
            //连接
            Connection connection = ConnectionUtil.getConnection();
            //获取管道
            final Channel channel = connection.createChannel();             
            //模拟真实场景,消费者接收到数据,并使用数据的时候会有一定的延迟--DeliverCallback实时缓冲数据,并告诉队列将信息以异步形式传递给我们
            int prefetchCount = 1;
            channel.basicQos(prefetchCount); //公平调度
            DeliverCallback callback = new DeliverCallback() {
                @Override
                public void handle(String s, Delivery delivery) throws IOException {
                    String msg = new String(delivery.getBody());
                    System.out.println("接收的消息========" + msg);
                    try {
                        //模仿处理任务所耗时长
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //新增finally块
                    finally {
                        //消息获取成功后ack响应
                        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    }
                }
            };
            //消费
            boolean autoAck = false; //关闭
            channel.basicConsume(QUEUE_NAME, autoAck, callback, consuer -> {
            });
            System.out.println("消费者1启动成功");
        } catch (
                Exception e) {
            e.printStackTrace();
        }
    }
}

 测试效果: ①消费者1:延迟0.5秒。②消费者2:延迟2.5秒。③生产者生产30条数据。
在这里插入图片描述
在这里插入图片描述

Ending

   书上有路勤为径,加油~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值