(10)RabbitMQ生产者确认:事务与confirm机制

上节介绍了RabbitMQ的消息持久化、Ack和重回队列,本节会继续介绍RabbitMQ的高级特性,将会介绍一下RabbitMQ的生产者确认的两种模式:事务与confirm机制。

概述

我们上一节介绍了RabbitMQ的事务机制,在RabbitMQ broker因为某些原因崩溃、重启时,可以确保消息不会丢失。但是我们发送完消息之后,并不知道消息有没有真的发到了RabbitMQ服务器上并存储完毕,如果因为网络闪断等原因导致消息没有发到服务器上,或者RabbitMQ服务器发生内部错误导致持久化失败,这样就会导致消息丢失。针对生产者发送消息的确认问题,RabbitMQ提供了如下两种方式(注意:事务机制跟confirm机制两者是互斥的,如果已经开启了其中一种,再去开启另外一种会报错的)

  1. 通过事务机制

  2. 通过发送方确认机制,即publish confirm模式,又可以细分为三种:单条confirm模式、批量confirm模式、异步confirm模式

注意:事务机制和publisher confirm 机制确保的是消息能够正确地发送至RabbitMQ的交换机,如果此交换机没有匹配的队列,那么消息也会丢失,并且生产者都会收到发送成功的消息(事务机制会返回Tx.Commit-OK,confirm机制会返回Basic.Ack)。如果想知道消息到底有没有路由到队列里面,发送消息时要结合mandatory 参数和ReturnListener(关于mandatory 参数和生产者的return机制下一节会介绍)。 

效率

效率从低到高为: 事务模式 < 单条confirm模式 < 批量confirm模式 < 异步confirm模式。

说明:单条confirm模式比事务模式效率仅高一点,批量confirm要比单条confirm的效率高很多,异步confirm模式要比批量confirm模式高一些,自己可以实际压测验证一下。

事务机制

提起事务,想必大家都很熟悉,在我们使用关系型数据库的时候经常使用事务,使用方法一般都是:先开启事务,然后操作数据,操作数据完成提交事务,如果操作失败进行事务的回滚。

RabbitMQ的事务机制操作过程跟上面的有点类似,主要有三个方法:

  1. channel.txSelect() 用于开启事务
  2. channel.txCommit() 用于提交事务
  3. channel.txRollback() 用于回滚事务

一般使用方式如下(关于事务机制下面已经给出关键,就不再单独贴完整的示例了,可以通过异常测试事务回滚的情况)

try {
	channel.txSelect();
	String message = "事务消息";
	channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        //可以模拟异常,回滚事务
	//int i=1/0;
	channel.txCommit();
} catch (Exception e) {
	e.printStackTrace();
	channel.txRollback();
}

RabbitMQ的事务机制操作流程图示如下:

事务机制主要有如下步骤:

  1. 客户端发送Tx.Select. 将信道置为事务模式;
  2. Broker 回复Tx. Select-Ok. 确认己将信道置为事务模式:
  3. 在发送完消息之后,客户端发送Tx.Commit 提交事务;
  4. Broker 回复Tx. Commit-Ok. 确认事务提交。

上面是事务可以正常提交的流程图,如果出现异常,则上面的步骤3和4变更为:客户端发送Tx.Rollback,Broker回复Tx.Rollback-OK。

注意:上面的代码示例中basicPublish方法中修改routing key为错误的参数,这时消息无法正确路由到队列上也会丢失,但是事务仍然可以提交成功,这也印证了我们上面所介绍的。只有消息被成功发送到RabbitMQ的交换机后事务才能够提交,否则捕获异常回滚事务,回滚事务之后也可以继续重发消息。事务机制是阻塞的,发送消息之后要一直等待RabbitMQ的回应,否则就无法发送下一条,其效率是最低的,不建议使用。

发送方确认机制

confirm机制概述

我们上面介绍了事务机制,但是效率太低了。RabbitMQ还提供了一种生产者确认(publisher confirm)的模式,消息生产者可以通过 channel.confirmSelect() 方法把channel开启confirm模式,通过confirm模式的channel发布的消息都会指定一个唯一的消息ID(也就是deliveryTag,从1开始递增)。消息被发到RabbitMQ后,RabbitMQ会给生产者发送消息,消息内容有:发送消息时传递过去的deliveryTag;一个标志Ack/Nack(Ack表示成功发到了RabbitMQ交换机上,Nack表示发送失败);还有一个multiple参数表示是否是批量确认,如果为false则表示单条确认,如果为true则表示到这个序号之前的所有消息都己经得到了处理。如果发送的是持久化消息,则在消息被成功写入磁盘之后才会发送给生产者确认消息。

事务模式吞吐量较低的原因是生产者每发送一条消息只能同步等待事务提交,然后才可以发送下一条。而confirm机制可以异步的处理,在生产者发送一条消息之后,可以在等RabbitMQ发送确认消息同时继续发送消息。RabbitMQ收到消息之后会发送一条Ack消息;如果消息服务器出现内部错误等原因导致消息丢失,会发送一条Nack消息。

注意:极少会出现nack的情况,一般都会返回ack的。要注意区分一下这里的ack跟上一节消费者消费消息时的ack,不要搞混了,消费者ack是表示消费者消费消息成功了,生产者收到RabbitMQ的ack表示生产者的消息成功投递到了RabbitMQ上了。

生产者confirm模式使用方式总共有三种: 单条confirm模式、批量confirm模式、异步confirm模式,这三种模式的开启方法都是 channel.confirmSelect()。

单条confirm

单条confirm模式就是发送一条等待确认一条,使用方式如下:在每发送一条消息就调用channel.waitForConfirms()方法,该方法等待直到自上次调用以来发布的所有消息都已被ack或nack,如果返回false表示消息投递失败,如果返回true表示消息投递成功。注意,如果当前信道没有开启confirm模式,调用waitforconfirms将引发IllegalstateException。

channel.confirmSelect();//将信道置为confirm模式
String message = "单条confirm消息";
channel.basicPublish(EXCHANGE_NAME, "", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
if(!channel.waitForConfirms()){
    System.out.println("消息发送失败");
    //进行重发等操作
}
System.out.println("消息发送成功");

另外,我们在上面说过"单条confirm模式的效率仅比事务模式高一点",这是为什么呢?其实通过上面的代码我们也看到了,单条confirm模式其实也是阻塞的,只不过它比事务模式少发了一个指令(事务机制发完消息之后要提交事务,然后等待RabbitMQ返回事务提交成功的消息;confirm模式发完消息后等待RabbitMQ的Ack)。

批量confirm

批量confirm模式就是先开启confirm模式,发送多条之后再调用waitForConfirms()方法确认,这样发送多条之后才会等待一次确认消息,效率比单条confirm模式高了许多。但是如果返回false或者超时,这一批次的消息就要全部重发,如果经常丢消息,效率并不比单条confirm高。。。。。。

channel.confirmSelect();//将信道置为confirm模式
for(int i=0;i<5;i++){
	String message = "批量confirm消息"+i;
	channel.basicPublish(EXCHANGE_NAME, "", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
}
if(!channel.waitForConfirms()){
	System.out.println("消息发送失败");
	//进行重发等操作
}
System.out.println("消息发送成功");

异步confirm

异步confirm模式是通过channel.addConfirmListener(ConfirmListener listener)方式实现的,ConfirmListener中提供了两个方法handleAck(long deliveryTag, boolean multiple) 和 handleNack(long deliveryTag, boolean multiple),两者分别对应RabbitMQ发送给生产者的ack和nack。方法中的deliveryTag就是上面说的发送消息的序号;multiple参数表示是否是批量确认。

异步confirm模式使用起来最为复杂,因为要自己维护一个已发送消息序号的集合,当收到RabbitMQ的confirm回调时需要从集合中删除对应的消息(multiple为false则删除一条,为true则删除多条),上面我们说过开启confirm模式后,channel上发送消息都会附带一个从1开始递增的deliveryTag序号,所以我们可以使用SortedSet的有序特性来维护这个发送序号集合:每次获取发送消息的序号存入集合,当收到ack时,如果multiple为false,则从集合中删除当前deliveryTag元素,如果multiple为true,则将集合中小于等于当前序号deliveryTag元素的集合清除,表示这批序号的消息都已经被ack了;nack的处理逻辑与此类似,只不过要结合具体的业务情况进行消息重发等操作。

其实RabbitMQ的java客户端就是用的SortedSet集合处理的,具体的可以查看com.rabbitmq.client.impl.ChannelN 这个类的代码,下面简单分析一下相对应的底层代码。

声明的SortedSet集合用于存储待确认的消息序号集合

/** Set of currently unconfirmed messages (i.e. messages that have
*  not been ack'd or nack'd by the server yet. */
private final SortedSet<Long> unconfirmedSet =
    Collections.synchronizedSortedSet(new TreeSet<Long>());

信道通过confirmSelect()方法开启confirm模式时,将消息发送序号置为1

 public Confirm.SelectOk confirmSelect()
        throws IOException
    {
        if (nextPublishSeqNo == 0) nextPublishSeqNo = 1;
        return (Confirm.SelectOk)
            exnWrappingRpc(new Confirm.Select(false)).getMethod();

    }

调用basicPulish 方法发送消息时,将当前序号存入SortedSet集合中

 public void basicPublish(String exchange, String routingKey,
                             boolean mandatory, boolean immediate,
                             BasicProperties props, byte[] body)
        throws IOException
    {
        if (nextPublishSeqNo > 0) {
            unconfirmedSet.add(getNextPublishSeqNo());
            nextPublishSeqNo++;
        }
        BasicProperties useProps = props;
        if (props == null) {
            useProps = MessageProperties.MINIMAL_BASIC;
        }
        transmit(new AMQCommand(new Basic.Publish.Builder()
                                        .exchange(exchange)
                                        .routingKey(routingKey)
                                        .mandatory(mandatory)
                                        .immediate(immediate)
                                        .build(),
                                       useProps, body));
        metricsCollector.basicPublish(this);
    }

 处理ack和nack时如下所示:也是从集合中移除对应的序号

 private void handleAckNack(long seqNo, boolean multiple, boolean nack) {
        if (multiple) {
            unconfirmedSet.headSet(seqNo + 1).clear();
        } else {
            unconfirmedSet.remove(seqNo);
        }
        synchronized (unconfirmedSet) {
            onlyAcksReceived = onlyAcksReceived && !nack;
            if (unconfirmedSet.isEmpty())
                unconfirmedSet.notifyAll();
        }
    }

 项目GitHub地址 https://github.com/RookieMember/RabbitMQ-Learning.git。下面是完整的代码示例,注意发完消息之后不要关闭连接,因为要等待回调确认消息。

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
/**
 * 
 * @ClassName:SendAsynConfirm
 * @Description:生产者confirm模式:异步confirm 
 * @author wkp
 * @date 2019年3月29日 下午10:10:01
 */
public class SendAsynConfirm {
 
    private final static String EXCHANGE_NAME = "fanout_exchange";
    //TreeSet是有序集合,元素使用其自然顺序进行排序,拥有存储需要confirm确认的消息序号
    static SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
 
    public static void main(String[] argv) throws Exception{
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout",true);
        //声明队列
        channel.queueDeclare("test_queue", true, false, false, null); 
        //绑定
        channel.queueBind("test_queue", EXCHANGE_NAME, "");
        
    	channel.confirmSelect();//将信道置为confirm模式
    	channel.addConfirmListener(new ConfirmListener() {
    		
    		public void handleNack(long deliveryTag, boolean multiple)
    				throws IOException {
			if (multiple) {
				confirmSet.headSet(deliveryTag + 1).clear();
			} else {
				confirmSet.remove(deliveryTag);
			}
    		}
    		
    		public void handleAck(long deliveryTag, boolean multiple)
    				throws IOException {
    			//confirmSet.headSet(n)方法返回当前集合中小于n的集合
    			if (multiple) {
    			    //批量确认:将集合中小于等于当前序号deliveryTag元素的集合清除,表示这批序号的消息都已经被ack了
    			    System.out.println("ack批量确认,deliveryTag:"+deliveryTag+",multiple:"+multiple+",当次确认消息序号集合:"+confirmSet.headSet(deliveryTag + 1));
				confirmSet.headSet(deliveryTag + 1).clear();
			} else {
				//单条确认:将当前的deliveryTag从集合中移除
				System.out.println("ack单条确认,deliveryTag:"+deliveryTag+",multiple:"+multiple+",当次确认消息序号:"+deliveryTag);
				confirmSet.remove(deliveryTag);
			}
    			//需要重发消息
    		}
    	});
    	
    	for(int i=0;i<30;i++){
    		String message = "异步confirm消息"+i;
    		//得到下次发送消息的序号
    		long nextPublishSeqNo = channel.getNextPublishSeqNo();
    		channel.basicPublish(EXCHANGE_NAME, "", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
    		//将序号存入集合中
    		confirmSet.add(nextPublishSeqNo);
    	}
        //关闭通道和连接
//        channel.close();
//        connection.close();
    }
}

 上面示例程序中,对确认的消息序号集合进行了打印,运行两次结果如下:

ack单条确认,deliveryTag:1,multiple:false,当次确认消息序号:1
ack批量确认,deliveryTag:30,multiple:true,当次确认消息序号集合:[2, 3, 4...(中间省略了)...28, 29, 30]
ack批量确认,deliveryTag:3,multiple:true,当次确认消息序号集合:[1, 2, 3]
ack批量确认,deliveryTag:30,multiple:true,当次确认消息序号集合:[4, 5, 6...(中间省略了)...29, 30]

多运行几次,可能每次都会不一样,你也可以尝试将发送消息总数改为50或者更多,然后多运行几次观察结果。通过运行结果发现:单个或者批量确认,貌似是随机的。。。。。。发送的消息条数越多,批量确认的次数越多,毕竟批量确认效率更高嘛。

好了,关于消息的生产者确认机制就先介绍到这里。下一节将会继续介绍RabbitMQ的高级特性:就是我们开头说到的mandatory参数和消息的return(ReturnListener)机制,欢迎继续关注。

参考 朱忠华《RabbitMQ实战指南》

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值