MQ!Rabbit-client 事务及消息确认机制
参考文档:
https://blog.csdn.net/u013256816/article/details/55515234
https://blog.csdn.net/hzw19920329/article/details/54315940
https://blog.csdn.net/anzhsoft/article/details/21603479
https://blog.csdn.net/hzw19920329/article/details/54340711
▥ ♬ゃō۰• •▥ ♬ゃō۰• •▥ ♬ゃō۰• •▥ ♬ゃō۰• •▥ ♬ゃō۰• •▥ ♬ゃō۰• •▥ ♬ゃō۰• •▥ ♬ゃō۰• •
本篇文章内容都是上面几篇内容的整合,写的也没上面参考资料写的好。如果对这部分内容感兴趣的强烈建议读上面的几篇博客(不是打广告🐷,而且这里也不适合打广告)。
事务机制
RabbitMQ中与事务机制有关的方法:txSelect()
, txCommit()
,txRollback()
, txSelect用于将当前channel设置成transaction模式,txCommit用于提交事务,txRollback用于回滚事务,在通过txSelect开启事务之后,我们便可以发布消息给broker代理服务器了,如果txCommit提交成功了,则消息一定到达了broker了,如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。
代码位于AMQImpl
中 MQ!Rabbit-client AMQImpl
public static class Tx {
public static final int INDEX = 90;
public static class Select
extends Method
implements com.rabbitmq.client.AMQP.Tx.Select
{
public static final int INDEX = 10;
public Select() {
}
public Select(MethodArgumentReader rdr) throws IOException {
this();
}
public int protocolClassId() { return 90; }
public int protocolMethodId() { return 10; }
public String protocolMethodName() { return "tx.select";}
public boolean hasContent() { return false; }
public Object visit(MethodVisitor visitor) throws IOException
{ return visitor.visit(this); }
public void appendArgumentDebugStringTo(StringBuilder acc) {
acc.append("()");
}
public void writeArgumentsTo(MethodArgumentWriter writer)
throws IOException
{
}
}
public static class SelectOk
extends Method
implements com.rabbitmq.client.AMQP.Tx.SelectOk
{
public static final int INDEX = 11;
public SelectOk() {
}
public SelectOk(MethodArgumentReader rdr) throws IOException {
this();
}
public int protocolClassId() { return 90; }
public int protocolMethodId() { return 11; }
public String protocolMethodName() { return "tx.select-ok";}
public boolean hasContent() { return false; }
public Object visit(MethodVisitor visitor) throws IOException
{ return visitor.visit(this); }
public void appendArgumentDebugStringTo(StringBuilder acc) {
acc.append("()");
}
public void writeArgumentsTo(MethodArgumentWriter writer)
throws IOException
{
}
}
public static class Commit
extends Method
implements com.rabbitmq.client.AMQP.Tx.Commit
{
public static final int INDEX = 20;
public Commit() {
}
public Commit(MethodArgumentReader rdr) throws IOException {
this();
}
public int protocolClassId() { return 90; }
public int protocolMethodId() { return 20; }
public String protocolMethodName() { return "tx.commit";}
public boolean hasContent() { return false; }
public Object visit(MethodVisitor visitor) throws IOException
{ return visitor.visit(this); }
public void appendArgumentDebugStringTo(StringBuilder acc) {
acc.append("()");
}
public void writeArgumentsTo(MethodArgumentWriter writer)
throws IOException
{
}
}
public static class CommitOk
extends Method
implements com.rabbitmq.client.AMQP.Tx.CommitOk
{
public static final int INDEX = 21;
public CommitOk() {
}
public CommitOk(MethodArgumentReader rdr) throws IOException {
this();
}
public int protocolClassId() { return 90; }
public int protocolMethodId() { return 21; }
public String protocolMethodName() { return "tx.commit-ok";}
public boolean hasContent() { return false; }
public Object visit(MethodVisitor visitor) throws IOException
{ return visitor.visit(this); }
public void appendArgumentDebugStringTo(StringBuilder acc) {
acc.append("()");
}
public void writeArgumentsTo(MethodArgumentWriter writer)
throws IOException
{
}
}
public static class Rollback
extends Method
implements com.rabbitmq.client.AMQP.Tx.Rollback
{
public static final int INDEX = 30;
public Rollback() {
}
public Rollback(MethodArgumentReader rdr) throws IOException {
this();
}
public int protocolClassId() { return 90; }
public int protocolMethodId() { return 30; }
public String protocolMethodName() { return "tx.rollback";}
public boolean hasContent() { return false; }
public Object visit(MethodVisitor visitor) throws IOException
{ return visitor.visit(this); }
public void appendArgumentDebugStringTo(StringBuilder acc) {
acc.append("()");
}
public void writeArgumentsTo(MethodArgumentWriter writer)
throws IOException
{
}
}
public static class RollbackOk
extends Method
implements com.rabbitmq.client.AMQP.Tx.RollbackOk
{
public static final int INDEX = 31;
public RollbackOk() {
}
public RollbackOk(MethodArgumentReader rdr) throws IOException {
this();
}
public int protocolClassId() { return 90; }
public int protocolMethodId() { return 31; }
public String protocolMethodName() { return "tx.rollback-ok";}
public boolean hasContent() { return false; }
public Object visit(MethodVisitor visitor) throws IOException
{ return visitor.visit(this); }
public void appendArgumentDebugStringTo(StringBuilder acc) {
acc.append("()");
}
public void writeArgumentsTo(MethodArgumentWriter writer)
throws IOException
{
}
}
}
事务提交步骤
- client发送Tx.Select
- broker发送Tx.Select-Ok(之后publish)
- client发送Tx.Commit
- broker发送Tx.Commit-Ok
代码示例:
try {
channel.txSelect();
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
// 触发异常,走回滚
int result = 1 / 0;
channel.txCommit();
} catch (Exception e) {
e.printStackTrace();
channel.txRollback();
}
Confirm模式
RabbitMQ可能会遇到的一个问题,即生成者不知道消息是否真正到达broker,随后通过AMQP协议层面为我们提供了事务机制解决了这个问题,但是采用事务机制实现会降低RabbitMQ的消息吞吐量,那么有没有更加高效的解决方式呢?答案是采用Confirm模式。
producer端confirm模式的实现原理
生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。
在channel 被设置成 confirm 模式之后,所有被 publish 的后续消息都将被 confirm(即 ack) 或者被nack一次。但是没有对消息被 confirm 的快慢做任何保证,并且同一条消息不会既被 confirm又被nack 。
开启confirm模式的方法
生产者通过调用channel的confirmSelect方法将channel设置为confirm模式,如果没有设置no-wait标志的话,broker会返回confirm.select-ok表示同意发送者将当前channel信道设置为confirm模式(从目前RabbitMQ最新版本3.6来看,如果调用了channel.confirmSelect方法,默认情况下是直接将no-wait设置成false的,也就是默认情况下broker是必须回传confirm.select-ok的)。
已经在transaction事务模式的channel是不能再设置成confirm模式的,即这两种模式是不能共存的。
客户端实现生产者confirm有三种编程方式
普通confirm模式:每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm了。
批量confirm模式:每发送一批消息后,调用waitForConfirms()方法,等待服务器端confirm。
异步confirm模式:提供一个回调方法,服务端confirm了一条或者多条消息后Client端会回调这个方法。
// 普通confirm模式
channel.basicPublish(ConfirmConfig.exchangeName, ConfirmConfig.routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, ConfirmConfig.msg_10B.getBytes());
if(!channel.waitForConfirms()){
System.out.println("send message failed.");
}
// 批量confirm模式
channel.confirmSelect();
for(int i=0;i<batchCount;i++){
channel.basicPublish(ConfirmConfig.exchangeName, ConfirmConfig.routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, ConfirmConfig.msg_10B.getBytes());
}
if(!channel.waitForConfirms()){
System.out.println("send message failed.");
}
// 异步confirm模式
SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Nack, SeqNo: " + deliveryTag + ", multiple: " + multiple);
if (multiple) {
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
});
while (true) {
long nextSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish(ConfirmConfig.exchangeName, ConfirmConfig.routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, ConfirmConfig.msg_10B.getBytes());
confirmSet.add(nextSeqNo);
}
消息确认(Consumer端)
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(ConfirmConfig.queueName, false, consumer);
while(true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String msg = new String(delivery.getBody());
// do something with msg.
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}