分布式事务(4)可靠消息最终一致性

分布式事务(1)基本概念
分布式事务(2)两阶段提交
分布式事务(3)TCC
分布式事务(4)可靠消息最终一致性
分布式事务(5)最大努力通知

1 可靠消息最终一致性事务的基本概念

可靠消息最终一致性是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理消息事务成功,此方案强调的是 只要消息发给事务参与方最终事务要达到一致

此方案是利用消息中间件完成:事务发起方(消息生产者)将消息发给消息中间件,事务参与方(消息消费者)从消息中间件接收消息。事务发起方和消息中间件之间,事务参与方和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。

2 问题产生分析

2.1 本地事务与消息发送的原子性问题

事务发起方在本地事务执行,1成功后消息必须发出去,2失败就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。

  • 方法一:先发送消息,再操作数据库。这种情况下无法保证 数据库操作与发送消息的一致性。因为可能发送消息成功,数据库操作失败。
begin transaction1.发送MQ消息
	2.数据库操作
commit transaction;
  • 方法二:先进行数据库操作,再发送消息:这种情况下:1、如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。2、如果MQ消息超时返回异常,数据库回滚,但MQ其实已经成功发送了,同样会导致不一致。
begin transaction1.数据库操作
	2.发送MQ消息
commit transaction;

2.2 事务参与方接收消息的可靠性

事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。

2.3 消息重复消费的问题

由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。
要解决消息重复消费的问题就要实现 事务参与方的方法幂等性

3 解决方案

3.1 本地消息表方案

本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后
通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
下面以注册送积分为例来说明:
下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。

交互流程如下:

  1. 用户注册
    用户服务在本地事务新增 “用户表” 和增加 “积分消息日志表”。(用户表和消息表通过本地事务保证一致)。这种情况下,本地用户表操作与存储积分消息日志表操作处于同一个数据库事务中,具备原子性。
begin transaction1.新增用户表数据
	2.存储积分消息日志表数据
commit transaction;
  1. 定时任务扫描日志: 如何保证将消息发送给消息队列呢?
    第一步消息数据已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件MQ,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。

  2. 消费消息: 如何保证消费者一定能消费到消息呢?
    这里可以使用MQ的 ack(即消息确认)机制,消费者监听 MQ,如果消费者接收到消息并且业务处理完成后向MQ发送 ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
    积分服务接收到“增加积分”消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复投递此消息。
    由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性。

3.2 RocketMQ事务消息方案

Apache RocketMQ 4.3之后的版本正式 支持事务消息,为分布式事务实现提供了便利性支持。

RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
在RocketMQ 4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。

以注册送积分的例子描述整个流程,执行流程如下:
消息生产者Producer 即 MQ发送方,本例中是用户服务,负责新增用户。
消息消费者Consumer 即 MQ订阅方,本例中是积分服务,负责新增积分。

  1. Producer 发送事务消息
    Producer (MQ消息发送方)发送事务消息至 MQ_Server,MQ_Server将消息状态标记为Prepared(预备状态),注意此时这条消息对于消费者(MQ消息订阅方)是无法消费到的。(本例中:Producer 发送 ”增加积分消息“ 到MQ Server)。

  2. MQ_Server 回应消息发送成功
    MQ_Server 接收到 Producer 发送给的消息则回应发送成功表示 MQ_Server 已接收到消息。

  3. Producer 执行本地事务
    Producer 端执行业务代码逻辑,通过本地数据库事务控制。(本例中:Producer 执行添加用户操作)。

  4. 消息投递
    1) 若Producer 本地事务执行成功则自动向MQServer发送commit消息,MQ Server接收到commit消息后将”增加积分消息“ 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
    2) 若Producer 本地事务执行失败则自动向MQServer发送rollback消息,MQ Server接收到rollback消息后 将删除”增加积分消息“ 。
    3) MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。

  5. 事务回查
    如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
    以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。

RocketMQ提供用于发送事务消息的API

	TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");
	producer.setNamesrvAddr("127.0.0.1:9876");
	producer.start();
	//设置TransactionListener实现
	producer.setTransactionListener(transactionListener);
	//发送事务消息
	SendResult sendResult = producer.sendMessageInTransaction(msg, null);

RoacketMQ提供RocketMQLocalTransactionListener接口

public interface RocketMQLocalTransactionListener {
    
    

	‐ 发送prepare消息成功此方法被回调,该方法用于执行本地事务
	‐ @param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id@param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
	‐ @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
	RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);@param msg 通过获取transactionId来判断这条消息的本地事务执行状态
	‐ @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
	RocketMQLocalTransactionState checkLocalTransaction(Message msg);
}

4 RocketMQ实现可靠消息最终一致性事务 demo

业务说明:
本例通过RocketMQ中间件实现可靠消息最终一致性分布式事务,模拟两个账户的转账交易过程。
两个账户在分别在不同的银行(张三在bank1、李四在bank2),bank1、bank2是两个微服务。交易过程是,张三给李四转账指定金额。(bank1张三扣减金额 与 bank2李四增加金额,两个操作必须是一个整体性的事务)。

4.1 demo组成说明

代码:https://gitee.com/michael_linux/distribution-tx
数据库:mysql 5.7.21
JDK:64位 1.8.0_191
rocketmq 服务端:rocketmq-all-4.8.0
rocketmq 客户端:rocketmq-spring-boot-starter.2.0.2.RELEASE
微服务框架:spring-boot-dependencies.2.1.3.RELEASEspring-cloud-dependencies.Greenwich.RELEASE
微服务及数据库的关系 :
distribution-tx/dtx-txmsg/dtx-txmsg-bank1 银行1,操作张三账户, 连接数据库bank1
distribution-tx/dtx-txmsg/dtx-txmsg-bank2 银行2,操作李四账户,连接数据库bank2

本示例程序技术架构如下:

交互流程如下:
1、bank1 向 MQ_Server 发送转账消息。
2、bank1 执行本地事务,扣减金额。
3、bank2 接收消息,执行本地事务,增加金额。

4.2 创建数据库

执行以下sql创建数据库及表结构基本数据

distribution-tx/sql/dtx-txmsg/bank1.sql
distribution-tx/sql/dtx-txmsg/bank2.sql

5.3.4.启动RocketMQ

rocketMQ版本:rocketmq-all-4.8.0-bin-release.zip.
rocketMQ实践:https://blog.csdn.net/Michael_lcf/article/details/106535406.

# 启动 nameserver
bin/mqnamesrv -n localhost:9876

# 启动 broker
bin/mqbroker -n localhost:9876 autoCreateTopicEnable=true

3.3.5 导入distribution-tx

distribution-tx/dtx-txmsg/dtx-txmsg-bank1 :操作张三账户,连接数据库bank1
distribution-tx/dtx-txmsg/dtx-txmsg-bank2 :操作李四账户,连接数据库bank2

5.3.8 测试场景

  • 测试1:成功转账。
    http://127.0.0.1:8081/bank1/transfer?accountNo=1&amount=100

  • 测试2:bank1本地事务失败,则bank1不发送转账消息。
    http://127.0.0.1:8081/bank1/transfer?accountNo=1&amount=3

  • 测试3:bank2接收转账消息失败,会进行重试发送消息。
    http://127.0.0.1:8081/bank1/transfer?accountNo=1&amount=4

  • 测试4:bank2多次消费同一个消息,实现幂等。

4.2 小结

可靠消息最终一致性就是保证消息从生产者经过消息中间件传递到消费者的一致性,本例使用了RocketMQ作为消息中间件,RocketMQ主要解决了两个功能:

  1. 本地事务与消息发送的原子性问题。
  2. 事务参与方接收消息的可靠性。

5 可靠消息最终一致性事务应用场景

可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。

猜你喜欢

转载自blog.csdn.net/Michael_lcf/article/details/128932659