步骤:
step1:
实线1向MQ发送消息,此时消息对consumer不可见,即不可被消费;
虚线1执行本地事务并提交事务。
step2:
执行本地事务的回调函数executeLocalTransaction()
可以有三种返回值:
LocalTransactionState.COMMIT_MESSAGE;
LocalTransactionState.ROLLBACK_MESSAGE;
LocalTransactionState.UNKNOW
将本地事务执行状态返回给broker,
如果是COMMIT_MESSAGE
,则将不可见消息变为可消费;
如果是ROLLBACK_MESSAGE
,broker会回收step1中的不可见消息;
如果是UNKNOW
或者producer与broker网络异常,broker会定时主动回调producer中的回调函数checkLocalTransaction()
去判断producer本地事务是否正常(一般是查询数据库中数据是否已经持久化),broker定时主动查看producer本地事务的逻辑应该是写在rocket server的代码中,目前尚未找到源码,但是查阅各种资料和博客,都有提到。
step3:
消费者消费消息。
step4:
如果有消息消费失败了,则将失败的消息回传给broker,即重新写入commitLog文件,消费者将重新消费;如果消息回传的时候,consumer和broker之间网络断开,则consumer会调用submitConsumeRequestLater()
方法,在consumer端进行重新消费,如果仍然消费失败,会不断重试直到达到默认的16次,你可以使用msg.getReconsumeTimes()
方法来获取当前重试次数,如果重试次数足够多之后仍然无法消费成功,必须通过工单、日志等方式进行人工干预以让producer事务进行回退处理。
疑问:
Question1:如何保持发送prepare消息和producer本地事务的事务一致性?
这个问题可以拆解成两个问题:
1、producer本地事务执行成功,但是prepare消息还没发送,怎么办?
prepare消息发送和producer本地事务执行是顺序执行的,只有prepare消息发送成功了,才会继续执行producer本地事务,因此,该种情况不存在,见代码:
public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter tranExecuter, final Object arg){
SendResult sendResult = null;
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
// 1. 第一阶段,发送 PREPARED 消息
try {
sendResult = this.send(msg);
} catch (Exception e) {
throw new MQClientException("send message Exception", e);
}
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
switch (sendResult.getSendStatus()) {
case SEND_OK: {
try {
// 第二阶段,执行本地事务操作
localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg);
if (null == localTransactionState) {
localTransactionState = LocalTransactionState.UNKNOW;
}
} catch (Throwable e) {
....
}
}
break;
case FLUSH_DISK_TIMEOUT:
case FLUSH_SLAVE_TIMEOUT:
case SLAVE_NOT_AVAILABLE:
localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
break;
default:
break;
}
// 到这一步,如果前面的都成功了,LocalTransactionState 应该是COMMIT_MESSAGE, 否则应该是 ROLLBACK_MESSAGE 或 UNKNOWN
// 第三阶段,会根据 LocalTransactionState 的值,发送不同类型的请求给 broker 去确认第一阶段发的消息。
try {
this.endTransaction(sendResult, localTransactionState, localException);
} catch (Exception e) {
.....
}
.....
}
2、prepare消息发送成功,但是producer本地事务执行失败,怎么办?
如step2中描述的,不管是producer本地事务执行失败,返回LocalTransactionState.UNKNOW
,还是producer与broker网络异常,broker都会定时主动回调producer中的回调函数checkLocalTransaction()
去判断producer本地事务是否正常(一般是查询数据库中数据是否已经持久化),返回LocalTransactionState.COMMIT_MESSAGE
或者LocalTransactionState.ROLLBACK_MESSAGE
同时producer本地手动回退(自己设计的回退方案,而非数据库的rollback)。
Question2:执行step3和step4时,网络断开怎么办,会影响分布式事务一致性吗?
先介绍两个概念:
强一致性:要求无论更新操作是在哪个数据副本上执行,之后所有的读操作都要能获得最新的数据。
最终一致性:用户读到某一操作对系统特定数据的更新需要一段时间,我们将这段时间称为"不一致性窗口",我们仅能保证用户最终能够读取到某操作对系统特定数据的更新。
因此,不管是step2、step3还是step4过程中网络断开,其实producer本地事务都已提交完成,都无法保证强一致性,因此TCC方案、MQ方案其实都是在保证事务的最终一致性,而非强一致性。
step3网络断开没关系,因为在pushconsumer+clustering的消费方式向,broker端保存了offset,无论consumer何时下线,一旦consumer上线,都会从offset处开始消费,我们不必为其操心。
step4网络断开怎么办?我们首先要明白step4存在的意义,step4之所以存在,是为了将消费失败的消息重新发回broker,让consumer进行重新消费,如果step4网络断开,是不是就意味着consumer无法重新消费这个消息了呢?非也,consumer端如果感知到网络断开,会立即将该消息存入失败队列,并重新消费失败队列中的消息,代码如下:
public void processConsumeResult(
final ConsumeConcurrentlyStatus status,
final ConsumeConcurrentlyContext context,
final ConsumeRequest consumeRequest
) {
int ackIndex = context.getAckIndex();
if (consumeRequest.getMsgs().isEmpty())
return;
switch (status) {
case CONSUME_SUCCESS:
if (ackIndex >= consumeRequest.getMsgs().size()) {
ackIndex = consumeRequest.getMsgs().size() - 1;
}
int ok = ackIndex + 1;
int failed = consumeRequest.getMsgs().size() - ok;
this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), ok);
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed);
break;
case RECONSUME_LATER:
ackIndex = -1;
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(),
consumeRequest.getMsgs().size());
break;
default:
break;
}
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
MessageExt msg = consumeRequest.getMsgs().get(i);
log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
}
break;
case CLUSTERING:
List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
MessageExt msg = consumeRequest.getMsgs().get(i);
//将所有消息都重新发送到broker中的commitLog文件,以让consumer重新消费
boolean result = this.sendMessageBack(msg, context);
//如果发送回broker的时候网络断开,则将这些消息加入msgBackFailed列表,立即重新消费,详情见submitConsumeRequestLater()函数
if (!result) {
msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
msgBackFailed.add(msg);
}
}
if (!msgBackFailed.isEmpty()) {
consumeRequest.getMsgs().removeAll(msgBackFailed);
this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
}
break;
default:
break;
}
long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
}
}
因此,step4虽然断开了,但我们最终目的是重新消费某些消费失败的消息,最终目的并没有受到影响,因此并不会影响我们的最终一致性。如果重新消费仍然失败,会不断重试直到达到默认的16次,你可以使用msg.getReconsumeTimes()
方法来获取当前重试次数,如果重试次数足够多之后仍然无法消费成功,必须通过工单、日志等方式进行人工干预以让producer事务进行回退处理。
说明:由于consumer可能会重新消费某些消息很多次,因此,应保证consumer端事务的幂等性,如insert into语句可以使用replace into来代替。
参考:
https://blog.csdn.net/zzzgd_666/article/details/80882107
http://www.iocoder.cn/RocketMQ/message-pull-and-consume-second/#defaultmqpushconsumerimplsendmessageback
https://help.aliyun.com/knowledge_detail/54318.html
https://blog.csdn.net/prestigeding/article/details/78998683
https://fdx321.github.io/2017/08/23/%E3%80%90RocketMQ%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E3%80%919-%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF/
https://mp.weixin.qq.com/s/vgohXl1LxYk3CyDI8WHxwA
https://cloud.tencent.com/developer/article/1015442
https://mp.weixin.qq.com/s/9A6ZnpBmAbQYC7kLr1iZCQ