书上有路勤为径,学海无涯苦作舟。
自述
RabbitMQ的第二个场景------工作队列/任务队列。
工作队列
工作队列(又名:任务队列)背后主要思想是避免立即执行资源密集型任务而不得不等待它完成,相反,我们安排任务稍后完成,将任务封装为消息并将其发送到队列中,在后台运行的工作进程将获取到该任务并执行该任务,当有多个工作者时,任务将在它们之间共享,简单理解:工作队列实现了在消费者之间分配任务。
工作队列模型分解
生产者将任务封装为消息发送到队列中,而消费者监听到队列中的消息就会进行获取,获取到消息之后将进行处理,工作队列和简单列队的区别在于:简单队列是一对一,而工作队列是一对多,在工作队列场景下,消费者是存在资源抢占的,处理数据较快的消费者,获取的数据自然多些,而处理数据慢的消费者,获取的数据也就少些。
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
书上有路勤为径,加油~