了解RocketMQ事务消息

143 阅读7分钟

了解RocketMQ事务消息

一、应用场景

为什么需要事务消息?试想一个分布式场景如电商下单,用户支付订单这一核心操作的同时会涉及到下游物流发货、积分变更、购物车状态清空等多个子系统的变更。

image.png

聪明的你可能会想到一个问题?如何保证核心业务和多个下游业务的执行结果完全一致?其实就是分布式事务需要解决的主要问题。

但是解决方式有很多,比如seata等分布式事务解决方案。为什么还要用事务消息呢,seata是专门的分布式解决方案,他们岂不是更专业?

那么我们想一想XA应该怎么做呢?可以将四个调用封装成包含四个独立事务分支的大事务啊。其实就是一个2pc,虽然XA简单易用,对业务无侵入,只需要在方法上加上注解即可,但是缺点也很显然易见,就是性能不足。当分支事务进入到阻塞阶段后,收到 XA commit 或 XA rollback 前必须阻塞等待,事务资源长时间得不到释放,锁定周期长,而且在应用层上面无法干预,性能差。感兴趣可以参考seata.apache.org/zh-cn/docs/…

这里就不详细解释AT和TCC了,我简单的说一下他们的优缺点,感兴趣的可以去Seata官网查看,不感兴趣可以直接跳过,对理解事务消息影响不大。

AT:Seata AT模式的核心是对业务无侵入,是一种改进后的两阶段提交。

一阶段:1.解析SQL,2.生成前置镜像,3.执行SQL,4.生成后置镜像,5.插入UNDO LOG前后镜像数据以及业务 SQL 相关的信息),6.向 TC 注册分支,7.本地事务提交,8将本地事务提交的结果上报给 TC。

二阶段-回滚:1.收到 TC 的分支回滚请求,开启一个本地事务2.查找到相应的 UNDO LOG 记录。3.数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。4.根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句。

二阶段-提交:1.收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。2.将异步和批量地删除相应 UNDO LOG 记录。

以下是我的愚见,非官方资料。

优点:无侵入、简单易用、分支事务无需阻塞等待

缺点:虽然分支事务无需阻塞等待,但是每次生成前置镜像、后置镜像、插入UNDO LOG,都需要一定时间。而且我感觉比较致命的问题:其它的请求修改了一阶段的数据,然后二阶段回滚时,与后置镜像不匹配会出现异常。

TCC:手工版的AT(自己瞎想的)。

一阶段 prepare 行为:调用 自定义 的 prepare 逻辑,对应AT一阶段。

二阶段 commit 行为:调用 自定义 的 commit 逻辑,对应AT二阶段-提交。

二阶段 rollback 行为:调用 自定义 的 rollback 逻辑,对应AT二阶段-回滚。

优点:TCC 完全不依赖底层数据库,能够实现跨数据库、跨应用资源管理,可以提供给业务方更细粒度的控制。

缺点:TCC 是一种侵入式的分布式事务解决方案,需要业务系统自行实现 Try,Confirm,Cancel 三个操作,对业务系统有着非常大的入侵性,设计相对复杂。

适用场景:TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。

为什么不用TCC而用事务消息呢?在这里就要介绍一下我们的业务场景:在服务器换卡续费的过程中:需要下单(生成订单,扣款,减库存),和创建一个新容器(这里面操作很多不是重点,理解一个5min的操作)。

如何保证一致性成为了很大困难,创建一个新容器并不属于一个数据库操作,所以直接排除XA和AT,在我看来,不适合TCC主要有以几点:

1.耦合性:比如我的上层服务是JAVA项目,但是创建容器是一个GO项目。但是使用TCC都必须实现TCC接口。

2.性能:TCC的Try阶段可能涉及资源预留,但是创建容器是一个耗时操作,导致预留时间过长。

3.稳定性:TCC以前存在幂等,空回滚,悬挂等问题,虽然已经修复,但是这种分布式事务框架容易出现问题,考虑到稳定性

4.其它:预留资源就需要修改表的结构加入冻结字段,这也不是我们不愿意看到的。

前面主要说了一些分布式事务框架的一些问题,既然用MQ我可以选择普通消息吗?

将上述基于XA事务的方案进行简化,拆分成一个本地事务和三条消息。

image.png

该方案中消息下游分支和订单系统变更的主分支很容易出现不一致的现象,例如:

1.消息发送成功,订单没有执行成功,需要回滚整个事务。

2.订单执行成功,消息没有发送成功,需要额外补偿才能发现不一致。

3.消息发送超时未知,此时无法判断需要回滚订单还是提交订单变更。

让我们仔细想一想如何解决这三个问题呢?

第一个,能不能等本地事务执行完,在消费消息?

第二个,能不能先发送消息,发送成功了在执行本地事务?

第三个,同二。

所以我们就像怎么把上面的方案完善一下呢?

1.先发送消息,但不能被消费(不可见)。

2.本地事务执行完,消息可被消费。

其实RocketMQ就是这么想的,就是它的方案更完善一些。

二、功能原理

事务消息交互流程如下图所示。

image.png

1.发送消息到服务端。

2.收到Ack确认消息已经发送成功,此时消息被标记为"暂不能投递"。(就是不被消费标记)

3.执行本地事务。

4.根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:

Commit:服务端将半事务消息标记为可投递,并投递给消费者。(消息我啊可以被消费了)

Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。(消息我啊被丢弃了)

5.服务端未收到发送者提交的二次确认结果,经过固定时间后,服务端将对消息生产者发起消息回查。

6.生产者收到消息回查后,会调用本地的回查方法,判断本地事务是否执行成功。

7.将6的结果返回给服务端。

可以看到我们刚刚就想到了14,那么57的回查有什么用呢?

试想一下,如果断网或者生产者重启了,服务端没有收到二次结果,那么在固定时间后,会回滚本地事务。虽然最终也能达到一致性,但是不是我们期望的,我们还是希望本地成功了之后,可以消费消息,而不是丢弃。这样对用户的体验就太差了。

扩展:在我们使用事务消息的时候一般都配合着一个定时任务使用。主要为了避免MQ挂掉或Mysql挂掉的情况,当然出现这种情况的概率极低。

三、源码

简单画了一下流程感兴趣的可以了解一下,可以看看我根据业务的优化。

image.png