大家好,我是苍何。
说起事务消息,你可能和我一样一开始有一些懵逼,但说起事务和分布式事务,我想对于八股选手来说,再熟悉不过了。
事务消息就是利用消息队列实现分布式事务的一种解决方案,而 RocketMQ 又有自己的实现方式。
一次性抛出来太多名词啦,让我们依次剖析,通过本文全面了解 RocketMQ 的事务消息,助力八股选手在线拷打面试官😀
什么是事务
我们先想象一个场景:
假设用户 A 向用户 B 转账 100 元,这个操作包括以下步骤:
- 从用户 A 的账户中扣减 100 元。
- 向用户 B 的账户中增加 100 元。
这两个步骤要么都成功,要么都失败。如果在步骤 1 扣减了100元,但步骤 2 增加时发生了错误,我们希望,用户A的账户金额恢复到原来的状态。
上面是一个典型的事务场景,什么是事务?,简而言之,事务是逻辑上的一组操作,要么都执行,要么都不执行。
下面有一道经典的八股,衡量事务的四大特性的了解程度,理解的背一下就好了。
事务的四大特性(ACID)
- 原子性(Atomicity):
- 事务中的所有操作要么全部成功,要么全部失败。即使在系统故障的情况下,事务也能保证不会只执行一部分操作。
- 例子:银行转账操作中,从一个账户扣钱并在另一个账户加钱,这两步操作要么都成功,要么都失败。
- 一致性(Consistency):
- 事务执行前后,数据库都必须处于一致的状态。所有事务必须使数据库从一个一致状态变换到另一个一致状态。
- 例子:转账后,两个账户的总金额应该保持不变。
- 隔离性(Isolation):
- 并发事务之间互不影响,一个事务的中间状态对其他事务不可见。不同事务之间的操作是相互独立的。
- 例子:同时进行的两个转账操作不会互相干扰,每个操作都看不到对方的中间状态。
- 持久性(Durability):
- 一旦事务提交,其结果是永久性的,即使系统崩溃,事务的结果也不会丢失。
- 例子:转账成功后,系统崩溃重启,账户金额的变动依然存在
什么是本地事务
在单体应用中的事务其实都属于本地事务,比如在 springboot 中在方法上加 @Transactional 注解,其实走的也是一种本地事务。
在单体中,通常一个操作,可能会涉及多张表,但都位于同一个数据库中,比如在 MySQL 中,一个事务可能包含多条 sql 语句的执行,这些语句要么执行成功,要么都执行失败,MySQL 支持事务的引擎是我们常用的 InnoDB,而 MyISAM 是不支持事务的。
MySQL 中主要是通过 redo log 和 undo log 来控制事务,redo log 是在事务提交前回滚,保证事务的持久性, undo log 是在事务提交后回滚,保证事务的原子性,具体可以看下面这张图:
什么是分布式事务
在分布式微服务系统中,原先的单体系统被拆成了多个微服务,比如 PmHub 中拆分了系统服务,项目服务、流程服务等,在实际应用中,每一个微服务可能会部署在不同的机器上,并且微服务的数据库是隔离的,比如 PmHub 中的 pmhub-project 微服务用的是 pmhub-project 数据库,pmhub-workflow 服务用的是 pmhub-workflow 数据库,
这种情况下,一个操作可能会涉及多个机器、多个服务、多个数据库。比如 PmHub 中的添加项目任务场景。
那么如何保证同一个操作,要么全部执行成功,要么全部执行失败呢?
那就需要用到分布式事务解决方案,本地事务解决的是单数据源的数据一致性问题,分布式事务解决的是多数据源数据一致性问题。
什么是事务消息
解决分布式事务,有多种成熟的方案,比如 2 PC、3 PC、TCC、本地消息、事务消息等。(这里的每一种方案要放细了说可以来个十万八千字,感兴趣的小伙伴可以先查资料学习一波)
我们今天的角儿是要通过事务消息来解决分布式事务,那什么是事务消息?
事务消息是一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。
而 RocketMQ 高级特性之一就是支持事务消息,基于 RocketMQ 实现的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。将二阶段提交和本地事务绑定,实现全局提交结果的最终一致性。
我来做个浅显的总结吧,事务消息也是消息的一种,只是比普通消息多了 plus 功能,那就是支持二阶段提交和回滚能力。
RocketMQ 事务消息实现原理
RocketMQ 采用了 2 PC 的方案来提交事务消息。
第一阶段,Producer 向 Broker 发送预处理消息(也称半事务消息),此时消息还未被投递出去,Consumer 不能消费,第二阶段,Producer 向 Broker 发送提交或回滚消息。
下面是具体的流程:
提交事务消息流程
- 发送预处理消息后,RocketMQ 会向 Producer 返回 Ack 确认消息已经发送成功
- Producer 开始执行本地事务
- 如果本地事务执行成功,Producer 发送提交事务消息
- 消息被投递给 Consumer,如下图所示:
回滚事务消息流程
- 发送预处理消息后,RocketMQ 会向 Producer 返回 Ack 确认消息已经发送成功
- Producer 开始执行本地事务
- 如果本地事务执行异常,Producer 发送提交回滚事务消息
- 服务端将回滚事务,消息不会被投递给 Consumer,如下图所示:
回查事务消息流程
如果本地事务状态为未知 Unknown,或者在断网或者是 Producer 应用重启等特殊情况下,若 Broker 未收到 Producer 提交的二次确认结果,一定时间后,Broker 会向 Producer 发起消息回查,来确认是提交还是回滚,如果回查也查不到,就需要人工介入了。
消息回查有点像是上学那会我们主动向老师提交作业,如果我们没提交,老师就会反过头来查我们,颇有一番相似。
实战——如何发送事务消息
在 PmHub 中通过 Seata 来处理添加任务过程中的分布式事务逻辑,这是原先逻辑流程:
Seata 实现核心代码:
现在我们也可以借助 RocketMQ 的事务消息来实现 PmHub 中任务管理的分布式事务。
1、先定义一个事务消息的Producer:
然后,我们修改TaskService类:
最后,修改原来的add方法:
1、预提交:在TaskTransactionProducer中的sendTaskMessage方法发送事务消息时触发。
2、本地事务:在executeLocalTransaction方法中执行,包括添加任务、添加任务成员和添加日志。
3、提交或回滚:根据本地事务的执行结果,在executeLocalTransaction方法返回相应的状态。
4、回查:在checkLocalTransaction方法中实现,用于处理本地事务执行结果未知的情况。
5、远程操作:在消息被确认提交后,消费者会处理消息并执行远程操作(任务指派消息提醒和添加或更新审批设置)。
这种方式将原来的操作分为了本地事务和远程操作两部分,通过RocketMQ的事务消息机制来保证整个过程的一致性。如果本地事务失败,消息不会被发送;如果远程操作失败,可以通过重试机制来保证最终一致性。
最后
其实 RocketMQ 通过事务消息实现分布式事务保证最终一致性,是性能比较好的解决方案,因其原理是 2 阶段提交,先预提交,然后根据本地事务的状态来决定最终是提交还是回滚。
刚开始接触可能会被很多新鲜的概念吓到,其实原理并不复杂。
需要特别注意 RocketMQ 事务的超时机制,即半事务消息被生产者发送到 Broker 后,如果在指定时间内服务端无法确认提交或者回滚状态,则消息默认会被回滚。
默认的话是 4 个小时,超过 4 个小时还没确定状态,半事务消息则会强制回滚,此时 broker 中的消息不回投递给消费者。
好啦,有关 RocketMQ 的事务消息就到这啦,有疑惑和问题欢迎大家留言讨论。
我是苍何,这是图解 RocketMQ 教程的第 10 篇,我们下篇见~