五事务型MQ的最终一致性事务方案--3如何保证事务成功及正确开发
五 事务型MQ的最终一致性事务方案--3 如何保证事务成功及正确开发
3.2.4 RocketMQ的事务消息机制,如何保证事务成功
1 理解RocketMQ如何保证整个事务流程一致性
在rocketMQ实现最终一致性事务时,系统中会出现如下几个问题,影响事务一致性:
1 producer发送失败;
2 producer发送消息,返回消息是否成功;
3 执行本地事务失败的处理;
4 本地事务执行,如果异常,此时如何解决
首先,对于producer.sendMessageInTransaction如果发送失败,那么调用端直接抛出异常,此时后续不会执行;
而对于producer发送half事务消息,根据第三章的源码跟踪可知,producer.send没有返回send_ok,那么不会执行executeLocal方法,那么后续会执行check回查时,一直查不到信息,最终会回滚消息,删除rollback;
对于执行本地事务失败的情况,这里是RocketMQ设计中的一个bug问题,其会catch掉异常,而不再抛出。所以如果执行事务MethodA>producer.send>executeLocal中执行事务消息入库,这个操作时,其不会是一个整体的事务------就是在executeLocal中抛出异常,不会被@Transactional的方法A所catch,因此出现事务不一致的情况。(该处bug在下面的小节中会展示源码)
2 rocketMQ的client在事务消息实现中的bug
事务消息的发送过程,在前面已经分析过源码,步骤如下:
STEP1 为消息添加prepared、producer-group属性。设置producerGroup目的是,在查询local事务状态时,可以从group中随机选择一个producer即可;
STEP2 调用同步发送,发送消息到rocketMQ;
STEP3 根据sendResult的发送状态,确定本地事务的状态
CASE1 OK当发送结果成功时,执行本地事务executeLocalTransaction方法,来设置事务执行状态;
CASE2 如果消息刷新超时,slave不可用,标记事务状态为rollback;
CASE3 其他情况,事务状态仍旧是unknown
STEP4 结束事务 this.endTransaction();
STEP5 构造事务消息发送的方法返回result;
2.1 producer.sendMessageInTransaction如果不对sendResult为ok判断
根据上述流程可知,本地事务的执行,需要再发送结果时send_ok的情况下,才会执行本地事务executeLocalTransaction。因此,常规情况下,需要对sendResult判断,如果返回不是send_ok,那么就抛异常。(因为,如果没有返回ok,那么由于网络的不确定性,可能是half消息没有发送成功、或者是broker的成功响应因为网络问题没有发送到producer端)。那么最好还是加上sendResult的返回结果判定。
如果不加结果判定,那么对于half后置一类,因为其executeLocalTransaction没有实际方法,影响不大(但是仍旧可能会出现half没有发送到broker端的隐患)
2.2 executeLocalTransaction抛异常问题
可以看到,在step3--》case1 发送结果返回ok的情况下,会执行本地方法executeLocalTransaction,如果本地事务异常,会被catch掉,但是没有重新throw,因此不会被调用方感知。
所以,如果最终一致性事务的实现方案是类似@Transactional add( serviceA--->producer.sendTransactionMessage-->saveTransaction本地事务)这一流程,如果在本地事务执行过程中,saveTransaction出现异常,当前操作不会成功,但是由于一场exception会被rocketMQ捕获,且不再继续抛出,因此异常不会被事务方法add感知,导致serviceA执行成功,但是本地事务不成功。然后执行回查时,会回滚消息。然后后续的serviceB不会消费到消息。
此时,造成事务不一致的结果:serviceA执行成功,但是serviceB无法消费到消息,事务无法执行。
在事务消息的发送过程的详细源码如下:
DefaultMQProducerImpl
public TransactionSendResult sendMessageInTransaction(final Message msg,
final LocalTransactionExecuter localTransactionExecuter, final Object arg)
throws MQClientException {
//this.defaultMQProducer,如果是TransactionMQProducer,则从中中获取监听器
TransactionListener transactionListener = getCheckListener();
if (null == localTransactionExecuter && null == transactionListener) {
throw new MQClientException("tranExecutor is null", null);
}
// 忽略配置的 DelayTimeLevel 延迟时间级别的参数
if (msg.getDelayTimeLevel() != 0) {
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
}
Validators.checkMessage(msg, this.defaultMQProducer);
SendResult sendResult = null;
/**…………………………………………………………………………………………为消息添加属性……………………………………………………………………………………………………*/
//STEP1 为消息添加prepared、producer-group属性。设置producerGroup目的是,在查询local事务状态时,可以从group中随机选择一个producer即可
//设置参数prepared为true
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
//设置消息所在的生产者组
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
try {
//TAG2 this.send(msg)
//STEP2 同步调用,发送消息到rocketMQ
sendResult = this.send(msg);
} catch (Exception e) {
throw new MQClientException("send message Exception", e);
}
/** …………………………………………………………………………根据sendResult的发送状态,确定本地事务的状态………………………………………………………………*/
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable localException = null;
switch (sendResult.getSendStatus()) {
case SEND_OK: {
try {
//为消息添加事务id的属性
if (sendResult.getTransactionId() != null) {
msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
}
String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
if (null != transactionId && !"".equals(transactionId)) {
msg.setTransactionId(transactionId);
}
//传入为null,不执行
if (null != localTransactionExecuter) {
localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
} else if (transactionListener != null) {
log.debug("Used new transaction API");
//step3 执行本地事务executeLocalTransaction方法
localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
}
if (null == localTransactionState) {
localTransactionState = LocalTransactionState.UNKNOW;
}
if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
log.info("executeLocalTransactionBranch return {}", localTransactionState);
log.info(msg.toString());
}
}
/**………………………………………………………………step3 执行本地事务executeLocalTransaction方法如果抛异常,这里会catch,并不再throw……………………………… */
catch (Throwable e) {
log.info("executeLocalTransactionBranch exception", e);
log.info(msg.toString());
localException = e; //在executeLocalTransaction执行中,如果抛出异常,会被catch掉,但是没有重新throw,因此不会被调用方感知
}
}
break;
case FLUSH_DISK_TIMEOUT:
case FLUSH_SLAVE_TIMEOUT:
case SLAVE_NOT_AVAILABLE:
localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
break;
default:
break;
}
//step4 结束事务
try {
this.endTransaction(sendResult, localTransactionState, localException);
} catch (Exception e) {
log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
}
//step5 构造事务消息发送的方法返回result
TransactionSendResult transactionSendResult = new TransactionSendResult();
transactionSendResult.setSendStatus(sendResult.getSendStatus());
transactionSendResult.setMessageQueue(sendResult.getMessageQueue());
transactionSendResult.setMsgId(sendResult.getMsgId());
transactionSendResult.setQueueOffset(sendResult.getQueueOffset());
transactionSendResult.setTransactionId(sendResult.getTransactionId());
transactionSendResult.setLocalTransactionState(localTransactionState);
return transactionSendResult;
}
3 RocketMQ事务最终一致性如何正确开发
在使用RocketMQ事务消息特性,实现分布式事务最终一致性时,是serviceA---》rocketMQ---》serviceB,同时根据rocketmq事务消息中两阶段执行的特点--先发送half事务消息,然后如果发送成功,执行本地事务,然后发送rollback/commit(如果server一定时间没有从client端获取到事务执行状态,那么会执行回查),回查需要查询本地的事务log情况,根据是否入库,判断serviceA是否执行成功。
根据如上的开发过程,会出现如下三种具体的实现方案:
下面看下三种不同实现方法,会出现什么问题:
3.1 发送half后置
当前half后置发送的开发,订单服务+事务消息落库+producer.sendHalf都是在一个事务中,并且注意去判断发送消息返回是否为send_ok。
分析当前开发模式,是否保证事务一致性:
1 订单数据或者事务消息有异常,由于同在一个事务中,因此事务回滚;
2 如果half消息发送异常,此时异常catch,log记录异常,同时throw抛出,外层事务方法可以感知,因此事务方法回滚;
3 对sendResult的发送结果判断事务是否发送成功,如果发送结果不是send_ok,那么需要抛出异常,此时执行事务回滚。(注意,如果send发送成功,就正常返回,当前事务执行成功)
------如果忽略对sendResult.ok的判断,那么就是发送完half消息就不管是否发送成功,那么half可能存在网络故障等问题不能成功发送
根据上述分析,事务流程能够保证各个方法调用出现异常的最终一致性。
3.2 发送half中置(本地方法中抛异常阻隔事务一致性)
half发送中置的情况,下单和producer.send是在同一个事务方法中,事务消息持久化在executeLocalTransaction方法中。
根据当前开发模式,分析其事务一致性在各个环节出现异常时,事务一致性是否能够保证:
1 如果下单异常,那么事务方法回滚,此时half的send不会执行,没问题;
2 如果half发送失败(发送失败、发送结果不为ok),事务回滚,此时没问题;
3 如果发送send_ok,那么此时下单操作完成,但是executeLocalTransaction执行失败:
case1 如果catch异常不抛出,返回rollback---此时事务消息落库失败,half消息rollback,但是由于没有throw,那么订单的事务方法感知不到异常。会出现订单落库、事务消息存储失败的情况;
case2 如果catch异常并throw,此时由于rocketmq代码的bug,此时异常会被catch,但是没有重新抛出,所以,订单的事务方法中感知不到事务异常,此时会出现同样的问题:下订单成功、事务日志持久化失败
因此,根据上述的分析,由于rocketMQ的bug,当前开发方案会有事务不一致的情况。不使用该方案。
3.3 发送half前置
当前模式,把业务方法和事务消息持久化的操作,统一放在executeLocalTransaction方法中。
分析当前方案,在各环节出现异常,一致性能否保证:
1 如果producer.sendHalf异常或者不是send_ok,那么抛异常。此时本地事务不执行;
2 如果本地事务executeLocalTransaction中抛异常:
由于调用事务方法,将订单落库+事务信息持久化放在一个@Transactional方法中,那么如果抛异常,当前事务回滚。不会落库。同时异常会抛向executeLocalTransaction方法,其被rocketMQ捕获,此时不会返回commit_message。那么broker不会得到事务状态,回去启动本地回查。
此时,当前方案开发没有什么问题。
总结:
上述三种方案,half消息中置方案,会因为executeLocalTransaction的异常bug问题,不能保证事务一致性,因此不予考虑。
但是其他两个方案,都可以使用。
但是,对于half消息前置,就是把所有的事务执行放在executeLocalTransaction中执行,会略微有些问题:
1 如果业务方法耗时,那么执行executeLocalTransaction的线程执行时间过长,可能会增加不必要的回查;而half消息后置,把业务方法先执行,那么会减少不必要的事务回查;
2 half消息前置,需要把更多的业务消息包装,通过sendMessageInTransaction(message,object)中object传输,那么会增加类型转换和model对象的构造、解析的代码。
4 consumer端如何保证事务一致性
对于分布式事务情况下,由于系统的复杂和流程繁复,如果consumer端消费失败而去执行回滚的话,需要付出更多的代价,而且还会引发其他系统回退导致的新问题。因此,面对这种概率低的事件,对于consumer端会返回reconsume_later,并重发消息,且默认重试16次,直到消费成功。如果失败,用人工介入处理。
考虑两种解决方案:
1 设置消息重试次数,如果达到指定次数,就发邮件或者短信通知人工介入;
2 等待消息重试次数超过默认的16次,进入死信队列,然后程序监听对应的私信队列主题,通知人工介入或者在rocketMQ控制台查看处理。
4.1 设置重试次数、人工介入
设置超时次数,例如3次,如果三次,需要发送邮件通知人工介入。然后再消息监听器messageListenerConcurrently中consumeMessage方法中,对消费异常情况catch捕获,并判断重试次数。
4.2 监听私信队列
消息队列的默认最大重试次数是16次,如果超过这个次数,消息会添加入死信队列---%DLQ%+consumerGroup消费者组名。例如:
然后点击TOPIC配置,修改其perm属性(2 write权限;4 读权限;6 读写权限)。
最后可以在程序中监听当前死信队列主题,如果有消息,通知人工介入。通过幂等式消费、对死信队列的监听,就保证rocketMQ的消息一定会被下游服务消费到。
5 RocketMQ事务消息的限制
- 事务消息不支持延时消息和批量消息。
- 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax 参数来修改此限制。如果已经检查某条消息超过 N 次的话 (N = transactionCheckMax) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionalMessageCheckListener 类来修改这个行为。
- 事务消息将在 Broker 配置文件中的参数 transactionTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionTimeout 参数。(该参数和代码部分,详细见3.2.3 回查事务状态部分的源码分析内容)
- 事务性消息可能不止一次被检查或消费。
- 提交给用户的目标主题消息可能会失败,它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
- 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ 服务器能通过它们的生产者 ID 查询到消费者。