分布式事务是一个复杂的问题,rmq实现了事务的最终一致性,rmq保证本地事务成功消息一定会发送成功并被成功消费,如果本地事务失败了,消息不会被发送。

rmq事务消息的实现过程为:

  1. producer发送half消息
  2. broker确认half消息,并通知producer,表示消息已经成功发送到broker(这个过程其实就是步骤1broker的返回)
  3. producer收到half确认消息之后,执行自己本地事务,并将事务结果(UNKNOW、commit、rollback)告诉broker(这是一个oneway消息,而且失败不重试)
  4. broker收到producer本地事务的结果后决定是否投递消息给consumer
  5. 鉴于producer发送本地事务结果可能失败,broker会定时扫描集群中的事务消息,然后回查(apache4.2.0尚未实现,因为没有调用org.apache.rocketmq.broker.client.net.Broker2Client#checkProducerTransactionState)

producer发送half消息

事务消息的发送过程和普通消息发送过程是不一样的,发送消息的方法是org.apache.rocketmq.client.producer.TransactionMQProducer#sendMessageInTransaction,入参有一个LocalTransactionExecuter,需要用户实现一个本地事务的executor,用户可以在executor中执行事务操作

  1. // org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendMessageInTransaction
  2. public TransactionSendResult sendMessageInTransaction(final Message msg,
  3. final LocalTransactionExecuter tranExecuter, final Object arg)
  4. throws MQClientException {
  5. if (null == tranExecuter) {
  6. throw new MQClientException("tranExecutor is null", null);
  7. }
  8. Validators.checkMessage(msg, this.defaultMQProducer);
  9. SendResult sendResult = null;
  10. // 标记消息是half消息
  11. MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
  12. MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
  13. try {
  14. // 发送half消息,该方法是同步发送,事务消息也必须是同步发送
  15. sendResult = this.send(msg);
  16. } catch (Exception e) {
  17. throw new MQClientException("send message Exception", e);
  18. }
  19. LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
  20. Throwable localException = null;
  21. switch (sendResult.getSendStatus()) {
  22. case SEND_OK: {
  23. // 只有在half消息发送成功的时候才会执行事务
  24. try {
  25. if (sendResult.getTransactionId() != null) {
  26. msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
  27. }
  28. // 执行本地事务
  29. localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg);
  30. if (null == localTransactionState) {
  31. localTransactionState = LocalTransactionState.UNKNOW;
  32. }
  33. if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
  34. log.info("executeLocalTransactionBranch return {}", localTransactionState);
  35. log.info(msg.toString());
  36. }
  37. } catch (Throwable e) {
  38. log.info("executeLocalTransactionBranch exception", e);
  39. log.info(msg.toString());
  40. localException = e;
  41. }
  42. }
  43. break;
  44. case FLUSH_DISK_TIMEOUT:
  45. case FLUSH_SLAVE_TIMEOUT:
  46. case SLAVE_NOT_AVAILABLE:
  47. localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
  48. break;
  49. default:
  50. break;
  51. }
  52. try {
  53. // 根据事务commit的情况来判断下一步操作
  54. this.endTransaction(sendResult, localTransactionState, localException);
  55. } catch (Exception e) {
  56. log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
  57. }
  58. TransactionSendResult transactionSendResult = new TransactionSendResult();
  59. transactionSendResult.setSendStatus(sendResult.getSendStatus());
  60. transactionSendResult.setMessageQueue(sendResult.getMessageQueue());
  61. transactionSendResult.setMsgId(sendResult.getMsgId());
  62. transactionSendResult.setQueueOffset(sendResult.getQueueOffset());
  63. transactionSendResult.setTransactionId(sendResult.getTransactionId());
  64. transactionSendResult.setLocalTransactionState(localTransactionState);
  65. return transactionSendResult;
  66. }

为了保证本地事务和消息发送成功的原子性,producer会先发送一个half消息到broker

  • 只有half消息发送成功了,事务才会被执行
  • 如果half消息发送失败了,事务不会被执行

half消息和普通的消息也不一样,half消息发送到broker后并不会被consumer消费掉。之所以不会被消费掉的原因如下:

  • broker在将消息写入CommitLog的时候会判断消息类型,如果是是prepare或者rollback消息,ConsumeQueue的offset(每个消息对应ConsumeQueue中的一个数据结构(包含topic、tag的hashCode、消息对应CommitLog的物理offset),offset表示数据结构是第几个)不会增加
  • broker在构造ConsumeQueue的时候会判断是否是prepare或者rollback消息,如果是这两种中的一种则不会将该消息放入ConsumeQueue,cnosumer在拉取消息的时候也就不会拉取到prepare和rollback的消息。

相关代码如下:

  1. // org.apache.rocketmq.store.CommitLog.DefaultAppendMessageCallback#doAppend(long, java.nio.ByteBuffer, int, org.apache.rocketmq.store.MessageExtBrokerInner)
  2. switch (tranType) {
  3. case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
  4. case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
  5. break;
  6. case MessageSysFlag.TRANSACTION_NOT_TYPE:
  7. case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
  8. // The next update ConsumeQueue information
  9. CommitLog.this.topicQueueTable.put(key, ++queueOffset);
  10. break;
  11. default:
  12. break;
  13. }
  14. // org.apache.rocketmq.store.DefaultMessageStore.CommitLogDispatcherBuildConsumeQueue#dispatch
  15. public void dispatch(DispatchRequest request) {
  16. final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
  17. switch (tranType) {
  18. case MessageSysFlag.TRANSACTION_NOT_TYPE:
  19. case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
  20. DefaultMessageStore.this.putMessagePositionInfo(request);
  21. break;
  22. case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
  23. case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
  24. break;
  25. }
  26. }

以上两点保证了prepare消息也就是half消息不会被消费。

producer结束事务

producer根据half消息发送结果和事务执行结果来处理事务——commit或者rollback。从上面发送消息的代码可以看到最后调用了endTransaction来处理事务执行结果,这个方法里面就是将事务执行的结果通过消息发送给broker,由broker决定消息是否投递。

  1. public void endTransaction(
  2. final SendResult sendResult,
  3. final LocalTransactionState localTransactionState,
  4. final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException {
  5. final MessageId id;
  6. // 从broker返回的信息中获取half消息的offset
  7. if (sendResult.getOffsetMsgId() != null) {
  8. id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId());
  9. } else {
  10. id = MessageDecoder.decodeMessageId(sendResult.getMsgId());
  11. }
  12. String transactionId = sendResult.getTransactionId();
  13. final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName());
  14. EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
  15. // 需要把transactionId和offset发送给broker,便于broker查找half消息
  16. requestHeader.setTransactionId(transactionId);
  17. requestHeader.setCommitLogOffset(id.getOffset());
  18. switch (localTransactionState) {
  19. case COMMIT_MESSAGE:
  20. // 表明本地址事务成功commit,告诉broker可以提交事务
  21. requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
  22. break;
  23. case ROLLBACK_MESSAGE:
  24. // 说明事物需要回滚,有可能是half消息发送失败,也有可能是本地事务执行失败
  25. requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
  26. break;
  27. case UNKNOW:
  28. // 如果状态是UNKNOW,broker还会反查producer,也就是接口:org.apache.rocketmq.example.transaction.TransactionCheckListenerImpl#checkLocalTransactionState的作用,但是目前rmq4.2.0并没有向producer查询,也就是源码中都没有调用这个接口
  29. requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
  30. break;
  31. default:
  32. break;
  33. }
  34. requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
  35. requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
  36. requestHeader.setMsgId(sendResult.getMsgId());
  37. String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
  38. // 这个发送消息是onway的,也就是不会等待返回
  39. this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
  40. this.defaultMQProducer.getSendMsgTimeout());
  41. }

从最一开始的图看出,producer将事务结果的消息发送给broker的时候可能会失败,如果失败了,broker就不知道本次事务是否应该commit,为了防止这种情况,rmq会向producer发送一个command查询处于prepare状态的事务的结果,上面也说了rmq4.2.0并没有发送这个command,也就是说当前rmq并不能保证producer将事务结果通知到broker。

broker决定消息是否可以投递

broker处理事务结果的消息的类是org.apache.rocketmq.broker.processor.EndTransactionProcessor

  1. 收到消息之后先检查是否是事务类型的消息,不是事务消息直接返回。
  2. 根据header中的offset查询half消息,查不到直接返回,不作处理
  3. 根据half消息构造新的消息,新构造的这个消息会被重新写入CommitLog,如果是rollback消息则body为空
  4. 如果是rollback消息的话,该消息不会被投递(原因和half不会被投递的原因一样),commit消息broker才会投递给consumer

也就是说rmq对于commit和rollback都会新写一个消息到CommitLog,只是rollback的消息的body是空的,而且该消息和half消息一样不会被投递,直到CommitLog删除过期消息,会从磁盘中删除;但是commit的时候,rmq会重新封装half消息并“投递”给consumer消费。

consumer保证消费成功

关于事务消息consumer端的消费方式和普通消息是一样的,RocketMQ能保证消息能被consumer收到(消息重试等机制,其实有可能存在consumer消费失败的情况,这种情况RocketMQ并不能解决,官方建议人工解决,这种情况出现的概率极低)。

总结

基于rmq的阿里云ons实现了事务最终一致性的所有功能,但是apache rmq没有实现消息回查的功能。所以rmq存在一定几率会让事务处于事务结果不明确的状态。

参考

收发事务消息

RocketMQ源码 — 十一、 RocketMQ事务消息的更多相关文章

  1. rocketmq源码分析4-事务消息实现原理

    为什么消息要具备事务能力 参见还是比较清晰的.简单的说 就是在你业务逻辑过程中,需要发送一条消息给订阅消息的人,但是期望是 此逻辑过程完全成功完成之后才能使订阅者收到消息.业务逻辑过程 假设是这样的: ...

  2. rocketmq源码分析2-broker的消息接收

    broker消息接收,假设接收的是一个普通消息(即没有事务),此处分析也只分析master上动作逻辑,不涉及ha. 1. 如何找到消息接收处理入口 可以通过broker的监听端口10911顺藤摸瓜式的 ...

  3. RocketMQ源码 — 三、 Producer消息发送过程

    Producer 消息发送 producer start producer启动过程如下图 public void start(final boolean startFactory) throws MQ ...

  4. RocketMQ源码 — 六、 RocketMQ高可用(1)

    高可用究竟指的是什么?请参考:关于高可用的系统 RocketMQ做了以下的事情来保证系统的高可用 多master部署,防止单点故障 消息冗余(主从结构),防止消息丢失 故障恢复(本篇暂不讨论) 那么问 ...

  5. RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)

    在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...

  6. RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想

    摘要: RocketMQ源码分析之从官方示例窥探RocketMQ事务消息实现基本思想. 在阅读本文前,若您对RocketMQ技术感兴趣,请加入RocketMQ技术交流群 RocketMQ4.3.0版本 ...

  7. RocketMQ源码详解 | Broker篇 · 其四:事务消息、批量消息、延迟消息

    概述 在上文中,我们讨论了消费者对于消息拉取的实现,对于 RocketMQ 这个黑盒的心脏部分,我们顺着消息的发送流程已经将其剖析了大半部分.本章我们不妨乘胜追击,接着讨论各种不同的消息的原理与实现. ...

  8. RocketMQ 源码学习笔记————Producer 是怎么将消息发送至 Broker 的?

    目录 RocketMQ 源码学习笔记----Producer 是怎么将消息发送至 Broker 的? 前言 项目结构 rocketmq-client 模块 DefaultMQProducerTest ...

  9. RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的?

    目录 RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 前言 项目结构 rocketmq-client 模块 DefaultMQProducerTest Roc ...

随机推荐

  1. JAVA物联网九大核心热点技术

    1. Unix/Linux平台技术: Unix系统原理.Unix常用命令.Shell编程.  2. Java EE核心技术: Java语言核心.Java高级API.JVM及性能优化.Java Secu ...

  2. 排序算法的C语言实现(下 线性时间排序:计数排序与基数排序)

    计数排序 计数排序是一种高效的线性排序. 它通过计算一个集合中元素出现的次数来确定集合如何排序.不同于插入排序.快速排序等基于元素比较的排序,计数排序是不需要进行元素比较的,而且它的运行效率要比效率为 ...

  3. python函数与装饰器

    一.名字空间与作用域 1.名字空间 名字空间:赋值语句创建了约束,用来存储约束的dict被称为名字空间      赋值语句的行为:1.分别在堆和栈中创建obj与name                 ...

  4. PHP基础(一)--字符串函数大盘点(基础篇)

    参考地址http://php.net/manual/zh/ref.strings.php addcslashes - 以 C 语言风格使用反斜线转义字符串中的字符    string addcslas ...

  5. SSRS 数据源访问Cube 无法创建订阅的解决方法

    SSRS Report 的数据源可以直接放问SSAS 的Cube. 当报表的数据源设置成下图: 这样设置后,report 能够正常访问 Cube 并打开Report. 但是,如果我们需要添加数据驱动的 ...

  6. php面向对象中的魔术方法

    原创,转载请注明出处 在 PHP 中以两个下划线开头的方法,__construct(), __destruct (), __call(), __callStatic(),__get(), __set( ...

  7. DevOps之二 Maven的安装与配置

    CentOS7 安装Maven 一.安装Maven mkdir -p /usr/local/maven3wget http://mirrors.hust.edu.cn/apache/maven/mav ...

  8. URI和URL的区别 【转】

    源地址:http://www.cnblogs.com/gaojing/archive/2012/02/04/2413626.html 这两天在写代码的时候,由于涉及到资源的位置,因此,需要在Java ...

  9. .NET开发微信小程序(基础配置)

    1.微信小程序的必备Model public class WxConfig { /// <summary> /// 小程序的appId /// 登录小程序可以直接看到 /// </s ...

  10. 【机器学习】使用gensim 的 doc2vec 实现文本相似度检测

    环境 Python3, gensim,jieba,numpy ,pandas 原理:文章转成向量,然后在计算两个向量的余弦值. Gensim gensim是一个python的自然语言处理库,能够将文档 ...