RabbitMQ消息确认机制
RabbitMQ消息确认的本质也就是为了解决RabbitMQ消息丢失问题,因为哪怕我们做了RabbitMQ持久化,其实也并不能保证解决我们的消息丢失问题
RabbitMQ的消息确认有两种
- 第一种是消息发送确认。这种是用来确认生产者将消息发送给交换器,交换器传递给队列的过程中,消息是否成功投递。发送确认分为两步,一是确认是否到达交换器,二是确认是否到达队列。
- 第二种是消费接收确认。这种是确认消费者是否成功消费了队列中的消息。
1.消息发送确认(生产者)
正常情况下,生产者会通过交换机发送消息至队列中,再由消费者来进行消费,但是其实RabbitMQ在接收到消息后,还需要一段时间消息才能存入磁盘,并且其实也不是每条消息都会存入磁盘,可能仅仅只保存到cache中,这时,如果RabbitMQ正巧发生崩溃,消息则就会丢失,所以为了避免该情况的发生,我们引入了生产者确认机制,rabbitmq对此提供了两种方式:
- 通过事务实现
- 通过发送方确认机制(
publisher confirm
)实现
事务实现
channel.txSelect()
: 将当前信道设置成事务模式channel.txCommit()
: 用于提交事务channel.txRollback()
: 用于回滚事务
通过事务实现机制,只有消息成功被rabbitmq服务器接收,事务才能提交成功,否则便可在捕获异常之后进行回滚,然后进行消息重发,但是事务非常影响rabbitmq的性能。还有就是事务机制是阻塞的过程,只有等待服务器回应之后才会处理下一条消息
/** * 创建生产者 */ public class Send {public static void main(String[] args) throws IOException, TimeoutException { //从MQ工具类获取连接信息 Connection connection = MqConnectionUtils.getConnection(); //创建一个通道 Channel channel = connection.createChannel(); //准备发送的消息内容 String msg = "你好"; //准备交换机(已创建的交换机) String exchangeName = "direct-exchange"; //准备路由 String routekey = "email"; try{ //将信道设置为事务模式 channel.txSelect(); //发送消息给交换机 /** * 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有 * 参数2:routekey * 参数3:消息的状态控制 * 参数4:消息内容 */ //该模式因为是由交换机发给该交换机绑定的所有队列,所以可以不标明队列名称 channel.basicPublish(exchangeName,routekey,null,msg.getBytes()); //事务提交 channel.txCommit(); System.out.print("发送成功"); } catch (Exception e){ //如果消息发送给交换机的过程出现异常,则捕捉并进行回滚 channel.txRollback(); System.out.print("发送失败并回滚"); } //关闭通道 channel.close(); connection.close(); } }
confirm实现
confirm方式有三种模式:普通confirm模式、批量confirm模式、异步confirm模式
channel.confirmSelect()
: 将当前信道设置成了confirm模式
普通confirm模式
每发送一条消息,就调用waitForConfirms()方法,等待服务端返回Ack或者nack消息
/** * 创建生产者 */ public class Send { public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { //从MQ工具类获取连接信息 Connection connection = MqConnectionUtils.getConnection(); //创建一个通道 Channel channel = connection.createChannel(); //准备发送的消息内容 String msg = "你好"; //准备交换机(已创建的交换机) String exchangeName = "direct-exchange"; //准备路由 String routekey = "email"; //将当前信道设置成confirm模式 channel.confirmSelect(); for(int i = 0;i<20;i++){ //发送消息给交换机 /** * 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有 * 参数2:routekey * 参数3:消息的状态控制 * 参数4:消息内容 */ //该模式因为是由交换机发给该交换机绑定的所有队列,所以可以不标明队列名称 channel.basicPublish(exchangeName,routekey,null,msg.getBytes()); //信道为confirm模式后,即可通过waitForConfirms()接收服务端返回来的信息 if(channel.waitForConfirms()){ System.out.print("发送成功"); } } final long start = System.currentTimeMillis(); System.out.println("执行waitForConfirmsOrDie耗费时间"+(System.currentTimeMillis()-start)+"ms"); //关闭通道 channel.close(); connection.close(); } }
批量confirm模式
每发送一批消息,就调用waitForConfirmsOrDie()方法,该方法会等到最后一条消息得到ack或者得到nack才会结束,也就是说在waitForConfirmsOrDie处才会造成程序的阻塞
/** * 创建生产者 */ public class Send { public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { //从MQ工具类获取连接信息 Connection connection = MqConnectionUtils.getConnection(); //创建一个通道 Channel channel = connection.createChannel(); //准备发送的消息内容 String msg = "你好"; //准备交换机(已创建的交换机) String exchangeName = "direct-exchange"; //准备路由 String routekey = "email"; //将当前信道设置成confirm模式 channel.confirmSelect(); for(int i = 0;i<20;i++){ //发送消息给交换机 /** * 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有 * 参数2:routekey * 参数3:消息的状态控制 * 参数4:消息内容 */ //该模式因为是由交换机发给该交换机绑定的所有队列,所以可以不标明队列名称 channel.basicPublish(exchangeName,routekey,null,msg.getBytes()); } final long start = System.currentTimeMillis(); //消息批量发送完成后,通过waitForConfirmsOrDie()方法来接收服务端返回的信息 channel.waitForConfirmsOrDie(); System.out.println("执行waitForConfirmsOrDie耗费时间"+(System.currentTimeMillis()-start)+"ms"); //关闭通道 channel.close(); connection.close(); } }
异步confirm模式
通过channel,addConfirmListener()监听发送方确认模式,通过信道中的waitForConfirmsOrDie等待传回ack或者nack
/** * 创建生产者 */ public class Send { public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { //从MQ工具类获取连接信息 Connection connection = MqConnectionUtils.getConnection(); //创建一个通道 Channel channel = connection.createChannel(); //准备发送的消息内容 String msg = "你好"; //准备交换机(已创建的交换机) String exchangeName = "fanout-exchanges"; //准备路由 String routekey = ""; //将当前信道设置成confirm模式 channel.confirmSelect(); for(int i = 0;i<100;i++){ msg= i + "chen"; //发送消息给交换机 /** * 参数1:交换机,不定义也会有默认的,因为我们的消息是通过交换机来进行投递给队列的,所以交换机不可能没有 * 参数2:routekey * 参数3:消息的状态控制 * 参数4:消息内容 */ //该模式因为是由交换机发给该交换机绑定的所有队列,所以可以不标明队列名称 channel.basicPublish(exchangeName,routekey,null,msg.getBytes()); } final long start = System.currentTimeMillis(); //通过addConfirmListener来监听信道 channel.addConfirmListener(new ConfirmListener() { //消息发送成功 @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.out.println("以确认消息:"+ deliveryTag + " 多个消息:" + multiple); } //消息发送失败 @Override public void handleNack(long deliveryTag, boolean multiple) throws IOException { System.out.println("no ack"); } }); System.out.println("执行waitForConfirmsOrDie耗费时间"+(System.currentTimeMillis()-start)+"ms"); //关闭通道 channel.close(); connection.close(); } }
2.消息接收确认(消费者)
消息接收确认机制,分为消息自动确认模式和消息手动确认模式,当消息确认后,我们队列中的消息将会移除
那这两种模式要如何选择呢?
- 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便。好处就是可以提高吞吐量,缺点就是会丢失消息
- 如果消息非常重要,不容丢失,则建议手动ACK,正常情况都是更建议使用手动ACK。虽然可以解决消息不会丢失的问题,但是可能会造成消费者过载
消息自动确认模式的实现
注:自动确认模式,消费者不会判断消费者是否成功接收到消息,也就是当我们程序代码有问题,我们的消息都会被自动确认,消息被自动确认了,我们队列就会移除该消息,这就会造成我们的消息丢失
/** * 消费者 */ public class Recv { //设定队列名称(已存在的队列) private static final String QUEUE_NAME = "queue1"; public static void main(String[] args) throws IOException, TimeoutException { //从mq工具类获取连接信息 Connection connection = MqConnectionUtils.getConnection(); //获取一个通道 Channel channel = connection.createChannel(); //监听该队列,true代表自动确认 channel.basicConsume(QUEUE_NAME,true,new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] body) throws IOException{ System.out.println("接收到的消息:"+ new String(body,"UTF-8")); } }); } }
实现效果,消费者会将我们队列中的消息全部接收然后确认,并移除队列
消息手动确认模式的实现
/** * 消费者 */ public class Recv { //设定队列名称(已存在的队列) private static final String QUEUE_NAME = "queue1"; public static void main(String[] args) throws IOException, TimeoutException { //从mq工具类获取连接信息 Connection connection = MqConnectionUtils.getConnection(); //获取一个通道 Channel channel = connection.createChannel(); //监听该队列,false代表手动确认 channel.basicConsume(QUEUE_NAME,false,new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] body) throws IOException{ System.out.println("接收到的消息:"+ new String(body,"UTF-8")); } }); } }
手动确认模式下,当我们消费者成功接收到消息后,在队列中消息会进入Unacked项,也就是待确认模式
所以我们还需要加上下列代码,来实现消息者在成功接收到消息后,手动确认
#添加红色字段
/** * 消费者 */ public class Recv { //设定队列名称(已存在的队列) private static final String QUEUE_NAME = "queue1"; public static void main(String[] args) throws IOException, TimeoutException { //从mq工具类获取连接信息 Connection connection = MqConnectionUtils.getConnection(); //获取一个通道 Channel channel = connection.createChannel(); //监听该队列 channel.basicConsume(QUEUE_NAME,false,new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties basicProperties, byte[] body) throws IOException{ System.out.println("接收到的消息:"+ new String(body,"UTF-8")); //获取消息的编号,我们需要根据消息的编号来确认消息 long tag = envelope.getDeliveryTag(); //获取当前内部类中的通道 Channel c = this.getChannel(); //手动确认消息,确认以后,则表示消息已经成功处理,消息就会从队列中移除,false代表只确认当前一个消息,true确认所有consumer获得的消息 c.basicAck(tag,false);
} }); } }
此时,我们的消息才会成功被确认,并移除队列