分布式事务工业界的解决方案

数据库事务的四大特性ACID:A(Atomic)原子性、C(Consistency)一致性、I(Isolation)隔离性、D(Durability)持久性。

而分布式事务无法满足ACID,针对分布式事务有CAP理论和Base理论。

CAP是Consistency、Availability、Partition tolerance的缩写,分别表示一致性、可用性、分区容错性。分布式事务的P是必须要有的,但在所有分布式事务场景中不能同时具备CAP三个特性,因为在具备了P的前提下C和A是不能共存的。AP是很多分布式系统设计时的选择,而CP是涉及金额时所选用的方案.

BASE是Basically Available、Soft state和Eventually consistent的缩写。BASE理论是对CAP中AP的一个扩展。允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为柔性事务.

分布式事务的解决方案:

1、使用seata实现2pc事务

(1)每个数据库都需要创建undo_log表,此表是seata保证本地事务一致性的关键

在各个库中创建undo_log表,此表为seata框架使用

 CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL, `log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

(2)启动TC(事务协调器)

[seata服务端解压路径]/bin/seata-server.bat -p 8888 -m file

(3)创建代理数据源

@Configuration
public class DatabaseConfiguration {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.ds0") 
    public DruidDataSource ds0() {
        DruidDataSource druidDataSource = new DruidDataSource(); 
        return druidDataSource;
    }
    @Primary
    @Bean
    public DataSource dataSource(DruidDataSource ds0)  {
        DataSourceProxy pds0 = new DataSourceProxy(ds0);
        return pds0;
    }
}

(4)全局事务开始使用@GlobalTransactional标识,每个本地事务方案仍然使用@Transactional标识

下面是bank1的账户A向bank2的账户B转账
分布式事务工业界的解决方案_第1张图片
分布式事务工业界的解决方案_第2张图片

2、TCC方案

tcc即try-confirm-cancel。tcc即自己写代码来实现事务的提交与回滚,此方案需要理解业务逻辑,而且tcc实现难度大,业务逻辑不同,tcc的实现也不同,没有统一的实现方案。
TCC需要注意三种异常处理分别是空回滚、幂等、悬挂。

空回滚:在没有调用TCC资源try方法的情况下,调用了二阶段的cancel方法,cancel方法需要识别出这是一个空回滚,然后直接返回成功。出现原因是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这时候其实没有执行try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的cancel方法,从而形成空回滚。

幂等:为了保证tcc二阶段提交重试机制不会引发数据不一致,要求tcc的二阶段try、confirm和cancel接口保证幂等,这样不会重复使用或者释放资源,如果幂等控制没有做好,很可能导致数据不一致等严重问题

悬挂:悬挂就是对于一个分布式事务,其二阶段cancel接口比try接口先执行。出现原因是在rpc调用分支事务try时,先注册分支事务,再执行rpc调用,如果此时rpc调用的网络发生拥堵,通常rpc调用是有超时时间的,rpc超时后,tm就会通知rm回滚该分布式事务,可能回滚完成后,rpc请求才到达参与者真正执行,而一个try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理,对于这种情况,我们称为悬挂,即业务资源预留后没法继续处理。解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。

示例:账户A向账户B转账

账户A
try:
    检查余额是否够30元
    扣减30元
confirm:
    空
cancel:
    增加30元
========================================================================
账户B
try:
    增加30元
confirm:
    空
cancel:
    减少30元

上述方案分析:

  1. 如果账户A的try没有执行在cancel则多加30元
  2. 由于try,cancel,confirm都是由单独的线程去调用,且会出现重复调用,所以都需要实现幂等
  3. 账号B在try中增加30元,当try执行完成后可能会其它线程给消费了,如果分布式事务异常,需要回滚,导致cancel阶段,金额不足以扣30元
  4. 如果账户B的try没有执行在cancel则多减30元

问题解决:

  1. 账户A的cancel方法需要判断try方法是否执行,正常执行try后方可执行cancel
  2. try,cancel,confirm方法实现幂等
  3. 账号B在try方法中不允许更新账户金额,在confirm中更新账户金额
  4. 账户B的cancel方法需要判断try方法是否执行,正常执行try后方可执行cancel

优化方案:

账户A
try:
    try幂等校验
    try悬挂处理
    检查余额是否够30元
    扣减30元
confirm:
    空
cancel:
    cancel幂等校验
    cancel空回滚处理
    增加可用余额30元
=============================================================
账户B
try:
    空
confirm:
    confirm幂等校验
    正式增加30元
cancel:
    空

3、可靠消息最终一致性

可靠消息最终一致性方案是指当事务发起方执行完本地事务后发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。此方案一般通过消息中间件实现。
此方案要解决以下几个问题:

(1)本地事务与消息发送的原子性问题

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

第一种方案:

begin transaction;
    // 1、发送MQ
    // 2、数据库操作
commit transaction;

上述方案,可能会出现消息发送成功,数据库操作失败,不可取

第二种方案

begin transaction;
    // 1、数据库操作
    // 2、发送MQ
commit transaction;

上述方案,MQ发送失败,抛出异常事务回滚,但如果是超时异常,数据库回滚,但MQ其实是已经发送了,同样会导致数据不一致

(2)事务参与方接收消息的可靠性

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

(3)消息重复消费问题

若某一个消费节点超时但消费成功,此时消息中间件会重复投递消息,需要事务参与方的方法幂等性

解决方案1:本地消息表方案

通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
举例:
分布式事务工业界的解决方案_第3张图片
(1)将新增用户和增加积分消息日志处于同一事务,间接保证了新增用户和发送增加积分消息进MQ的原子性
(2)定时任务扫描日志,扫描日志表中的消息并发送到消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试
(3)使用MQ的ack(即消息确认)机制来保证消费者一定能消费到消息。由于消息会重复投递,消费方需要实现幂等性

解决方案2:RocketMQ事务消息方案

RocketMQ事务消息设计则主要是为了解决Producer端的消息发送与本地事务执行的原子性问题,RocketMQ的设计中的broker与producer端双向通信能力,使得broker天生可以作为一个事务协调者存在。
分布式事务工业界的解决方案_第4张图片
(1)Producer(MQ发送方)发送事务至MQ Server,MQ Server将消息状态标记为Prepared状态,注意此时这条消息消费者(MQ订阅方)是无法消费到的

(2)MQ Server接收到Producer发送给的消息则回应发送成功表示MQ已接收到消息

(3)Producer端执行业务代码逻辑,通过本地数据库事务控制

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

(5)事务回查,如果Producer执行本地事务中,执行端挂掉或超时,MQ Server将会不停地询问同组的其它Producer来获取事务执行状态。MQ Server会根据事务回查结果来决定是否投递消息。

用户需要实现本地事务执行以及本地事务回查方法

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、最大努力通知

举例:
分布式事务工业界的解决方案_第5张图片
最大努力通知与可靠消息一致性有什么不同?
(1)解决方案思想不同
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发送到接收通知方,消息的可靠性关键由发起通知方来保证
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性在接收通知方

(2)两者的业务应用场景不同
可靠消息一致性关注的是交易过程中的事务一致,以异步方式完成交易
最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去

(3)技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到
最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠性机制的是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)

最大努力通知有两个方案
方案1:
分布式事务工业界的解决方案_第6张图片
方案2:
分布式事务工业界的解决方案_第7张图片
方案1中接收通知方与MQ接口,即接收通知方监听MQ,此方案主要应用于内部应用之间的通知
方案2中由通知程序与MQ接口,通知程序监听MQ,收到MQ的消息后由通知程序通过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知,例如支付宝、微信的支付结果通知

你可能感兴趣的:(分布式事务工业界的解决方案)