事务消息的日志(RocketMQ )

前言

提示:这篇文章主要是我通过查找各种资料然后了解熟悉事务消息日志,以及使用 RocketMQ 事务消息,实现分布事务,RocketMQ的优缺点等,还在不断的了解当中,希望通过我查找记录的内容,对后续的使用会有所帮助。


一、什么是事务消息(Transactional message)?

RocketMQ官方是这样定义的。可以将其视为两阶段提交消息实现,以确保分布式系统中的最终一致性。事务性消息确保可以原子方式执行本地事务的执行和消息的发送

1、事务状态

事务消息有3种状态

(1)TransactionStatus.CommitTransaction,提交事务,表示允许消费者消费(使用)这条消息

(2)TransactionStatus.RollbackTransaction,回滚事务,表示消息将被删除,不允许使用

(3)TransactionStatus.Unknown,中间状态,表示需要MQ向消息发送方进行检查以确定状态

2、如何发送事务消息

RocketMQ(4.5.1版本)已经把事务消息的发送方式封装得非常优雅,只需要两个大的环节就能够完成,创建事务消息生产者和实现TransactionListener接口。可以查看官方的代码。

(1)创建事务消息生产者

使用TransactionMqProducer类创建消息生产客户端,并指定唯一的ProducerGroup

设置自定义线程池来处理检查请求

执行本地事务之后,需要根据执行结果回复MQ,回复上一小节中描述的状态

(2)实现TransactionListener接口

“executeLocalTransaction”方法用于在发送半条消息成功时执行本地事务。它返回上一节中提到的三个事务状态之一。

“check local transaction”方法用于检查本地事务状态并响应MQ检查请求。它还返回前一节中提到的三个事务状态之一。

3、事务消息的执行流程

代码写起来非常简单,以至于光看代码,并不能知道事务消息具体的执行过程。

RocketMQ 事务消息的设计流程借鉴了两阶段提交理论,整体交互流程如下图所示

在这里插入图片描述

事务发起方(即消息发送者)首先发送 prepare 消息到 MQ。

事务发起方(即消息发送者)在发送 prepare 消息成功后执行本地事务。

根据本地事务执行结果发送 commit 或者是 rollback 给 MQ。

如果消息是 rollback,MQ 将删除该 prepare 消息不进行下发。

如果消息是 commit,MQ 将会把这个消息发送给 consumer 端。

如果执行本地事务过程中,执行端挂掉,或者超时,导致 MQ 收不到任何的消息(不知道是该 commit 还是该 rollback),RocketMQ 会定期扫描消息集群中的事务消息,这时候发现了某个 prepare 消息还不知道该怎么处理,它会向消息发送者确认,所以消息发送者需要实现一个 check 接口,RocketMQ 会根据消息发送者设置的策略来决定是 rollback 还是继续 commit。这样就保证了消息发送与本地事务同时成功或同时失败。

Consumer 端的消费成功机制由 MQ 保证。

4、事务消息的存储模型

在具体实现上,RocketMQ 通过使用 Half Topic 以及 Operation Topic 两个内部队列来存储事务消息推进状态,如下图所示

在这里插入图片描述

其中,Half Topic 对应队列中存放着 prepare 消息,Operation Topic 对应的队列则存放了 prepare message 对应的 commit/rollback 消息,消息体中则是 prepare message 对应的 offset,服务端通过比对两个队列的差值来找到尚未提交的超时事务,进行回查。

从用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可;而在 service 层,则对事务消息的两阶段提交进行了抽象,同时针对超时事务实现了回查逻辑,通过不断扫描当前事务推进状态,来不断反向请求 Producer 端获取超时事务的执行状态,在避免事务挂起的同时,也避免了 Producer 端的单点故障。

而在存储层,RocketMQ 通过 Bridge 封装了与底层队列存储的相关操作,用以操作两个对应的内部队列,用户也可以依赖其他存储介质实现自己的 service,RocketMQ 会通过 ServiceProvider 加载进来。

二、Notify的异曲同工

Notify和MetaQ是阿里的两个消息中间件。MetaQ是一个高性能的存储队列;Notify是淘宝自主研发的一套消息服务引擎。贴两个图就什么都明白了
在这里插入图片描述

整体方案跟RocketMQ是完全相同的,只是两者的Storage不同。

三、瓜子该怎么做事务一致性这块工作

针对这个典型场景,有很多解决方案

1、Kafka换成RocketMQ

不行。有太多的业务跑在了Kafka上,替换消息中间件的成本基本不能接受。

2、类似去哪儿qmq的方案(《写数据库同时发mq消息事务一致性的一种解决方案》)

这个方案研发简单,但是侵入具体业务的数据库,而且增加了部署运维的成本。

3、有人提出binlog+TCC的方案

没有仔细研究,但是业务会经常调整,想想负责配置数据库日志的同学肯定会抓狂(DBA没有那么了解业务)。

4、为Kafka配一个类似Notify的消息引擎

这个方案有一定的可行性

(1)把Kafka定位为MetaQ,研制一个Notify,为prepare message提供单独的存储

(2)现在各业务系统所采用的Kafka客户端已经是瓜子定制化开发的,可以模仿RocketMQ的客户端进行改造。已有代码的逻辑完全不受影响;需要事务一致性的功能,只需要换个接口,实现check逻辑即可,而原有消费方毫无感觉。

(3)似乎有可能结合spring的@Transactional标签,在完全不改业务代码(只升级自研Kafka客户端)的情况下,也能缓解一些不一致问题(《事务注解(@Transactional)引起的数据覆盖故障》)

分布式事务利器——RocketMQ事务消息的启示

rocketMQ解决分布式事务的思路:

1、a事务成功和mq收到消息保持一致。
2、保证这条消息一定会被消费,从而完成b事务。时效性可能差了点,但是能达到最终的一致,优点是不会阻塞。

其中第二步保证消息一定会被消费可以看之前的博文,消费端用集群模式可以做到这一点。

下面来看怎样使得 事务a成功 和 mq收到消息 保持一致:

sendMessageInTransaction中,往header中放PROPERTY_TRANSACTION_PREPARED属性,值为true,请求来到了broker这边的SendMessageProcessor.sendMessage方法中,判断刚才的值为true的话,调用getTransactionalMessageService().prepareMessage,把message原来的topic和queueId隐藏起来,替换为topic: RMQ_SYS_TRANS_HALF_TOPIC queueId : 0,然后存入commitLog中,这个topic是没人订阅的所以不会被消费,发送成功后,执行a事务(这里执行a事务是在transactionListener.executeLocalTransaction方法中,transactionListener还有一个方法是checkLocalTransaction,这个方法是查看事务是否完成,逻辑都是需要用户自己写,两者之间要一致)。

正常情况:如果事务a成功,会发送endTransaction的消息,broker收到之后会把RMQ_SYS_TRANS_HALF_TOPIC 中的消息取出来放到事务消息真实对应的topic中,被b消费。

异常情况:

1、half请求broker没收到,broker将不会存储half消息,producer也不会执行下面的事务。

2、half请求broker收到,但是producer没有成功收到broker的响应,那么producer暂时不会执行自己的事务,broker会有回查机制,发现producer没有执行事务,就不会把half消息放到真实topic中。

3、成功发送half消息,但是提交事务或者回滚事务的消息broker没有收到,还是会被回查。

回查的代码逻辑:broker的处理方式是间隔一定时间扫描half消息,然后发出请求向producer回查(间隔1分钟,最多15次),逻辑是BrokerController.start—>startProcessorByHa—>transactionalMessageCheckService.start()—>waitForRunning—>onWaitEnd—>getTransactionalMessageService().check—>listener.resolveHalfMsg—>sendCheckMessage给consumer,consumer收到CHECK_TRANSACTION_STATE消息,checkTransactionState—>producer.checkTransactionState—>transactionListener.checkLocalTransaction(如上文所说,检查本地a事务的情况)—>processTransactionState—>endTransactionOneway(再次提交事务)

该方案关键是要有个消息表。另外,一般会有个队列,而且我们一般都会假设这个MQ不丢消息。

最终一致性消息模型

基本思路如下

消息生产方
需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交(有时实现时为了简单,可以只是增加一个字段。新增字段会跟业务强耦合)

消息消费方
需要处理这个消息,并完成自己的业务逻辑。
如果本地事务处理成功,那发送给生产方一个confirm消息,表明已经处理成功了。

如果处理失败,可以发送给生产方一个失败消息,或者记录本地表。以后重试。

生产方定时扫描本地消息表,把还没处理完成的消息重新发送一遍。如果有其他的对账补账逻辑,这一步也可以省略。

整体流程如下图所示:
在这里插入图片描述
对一致性要求不高的或者有其他兜底方案的场景(比如较为频繁的对账补账机制),我们就不需要关心消息的confirm等情况,只要扔给消息,就认为OK,一般也是可取的。

但是这个方案存在一个小问题

如果发送消息失败,发送方并不知道是消息中间件真的没有收到消息呢,还是消息已经收到了只是返回response的时候失败了

如果是已经收到消息了,而发送端认为没有收到,执行update db的回滚操作。则会导致发送方事务回滚,接受方却把后续逻辑做掉了

把网络调用放在DB事务里面,可能会因为网络的延时,导致DB长事务,存在风险

事务消息模型

国内常用方案
事务消息与上面提到的最终一致性模型一致,只不过是把记录消息发送状态这一步在中间件内部做掉了,从而无需业务方针对每个业务自己手动实现。

具体的实现如下图所示。
在这里插入图片描述

事务消息组件的套路

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值