分布式事务解决方案

应用场景

  • 支付

最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。

  • 下单

买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。

方案一:事务型消息中间件+最终一致性

例如:RocketMQ
在这里插入图片描述

sequenceDiagram
A->>mq: 1、发布消息
mq->>mq:2、消息持久化
mq-->>A:3、返回应答
A->>A:处理A任务成功/失败
A->>mq:4、commit/rollback
mq->>B:5、投递消息
B->>B:处理任务B成功
B-->>mq:6、返回应答
  1. 在系统A处理任务A前,首先向消息中间件发送一条消息
  2. 消息中间件收到后将该条消息持久化,但并不投递。此时下游系统B仍然不知道该条消息的存在。
  3. 消息中间件持久化成功后,便向系统A返回一个确认应答;
  4. 系统A收到确认应答后,则可以开始处理任务A;
  5. 任务A处理完成后,向消息中间件发送Commit请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了,此时它可以处理别的任务了。但commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。
  6. 消息中间件收到Commit指令后,便向系统B投递该消息,从而触发任务B的执行;
  7. 当任务B执行完成后,系统B向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,消息中间件删除此条消息,此时,这个分布式事务完成。

上述过程可以得出如下几个结论:

  1. 消息中间件扮演者分布式事务协调者的角色。
  2. 系统A完成任务A后,到任务B执行完成之间,会存在一定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是可以接受的,因为经过短暂的时间后,系统又可以保持数据一致性,满足BASE理论

异常考虑

  • 步骤一、二、三处理失败失败:

原因多种,例如步骤一因为网络异常失败,步骤二因为MQ初始化失败,步骤三因为网络不稳定造成丢包,造成Http请求超时等,代码层面就是调用MQ接口,总之返回结果就是失败。此时对于A系统处理方法,第一:直接返回向上抛出错误,第二:可以加入重试机制,但是前提是一定要保障信息消费的幂等性;

  • 步骤四失败–超时询问机制:

两种原因产生,第一:MQ服务不可用;第二:消息在网络传输中丢失。处理方案是,事务类型MQ都提供了事务询问接口。系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到一条事务型消息后便开始计时,如果到了超时时间也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果:

提交:若获得的状态是“提交”,则将该消息投递给系统B;

回滚:若获得的状态是“回滚”,则直接将条消息丢弃;

处理中:若获得的状态是“处理中”,则继续等待。

消息中间件的超时询问机制能够防止上游系统因在传输过程中丢失Commit/Rollback指令而导致的系统不一致情况,而且能降低上游系统的阻塞时间,上游系统只要发出Commit/Rollback指令后便可以处理其他任务,无需等待确认应答。而Commit/Rollback指令丢失的情况通过超时询问机制来弥补,这样大大降低上游系统的阻塞时间,提升系统的并发度。

  • 步骤五、六失败 – 超时重试 AND 人工干预

同样两种原因:第一:服务B不可用;第二:消息在网络传输中丢失;

消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕!

如果消息在投递过程中丢失,或消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。当然,一般消息中间件可以设置消息重试的次数和时间间隔,比如:当第一次投递失败后,每隔五分钟重试一次,一共重试3次。如果重试3次之后仍然投递失败,那么消息中间件不再投递此消息,而是把此消息放到失败队列表中,同时消息中间件提供失败消息查询接口,下游系统会定期的查询失败消息,并将其消费消费掉,这也叫做“定期校对”,如果此时还是不能解决问题,那么这条消息就需要人工干预

有的同学可能要问:消息投递失败后为什么不回滚消息,而是不断尝试重新投递?

这就涉及到整套分布式事务系统的实现成本问题。我们知道,当系统A将向消息中间件发送Commit指令后,它便去做别的事情了。如果此时消息投递失败,需要回滚的话,就需要让系统A事先提供回滚接口,这无疑增加了额外的开发成本,业务系统的复杂度也将提高。对于一个业务系统的设计目标是,在保证性能的前提下,最大限度地降低系统复杂度,从而能够降低系统的运维成本

不知大家是否发现,上游系统A向消息中间件提交Commit/Rollback消息采用的是异步方式,也就是当上游系统提交完消息后便可以去做别的事情,接下来提交、回滚就完全交给消息中间件来完成,并且完全信任消息中间件,认为它一定能正确地完成事务的提交或回滚。然而,消息中间件向下游系统投递消息的过程是同步的。也就是消息中间件将消息投递给下游系统后,它会阻塞等待,等下游系统成功处理完任务返回确认应答后才取消阻塞等待。为什么这两者在设计上是不一致的呢?

首先,上游系统和消息中间件之间采用异步通信是为了提高系统并发度。业务系统直接和用户打交道,用户体验尤为重要,因此这种异步通信方式能够极大程度地降低用户等待时间。此外,异步通信相对于同步通信而言,没有了长时间的阻塞等待,因此系统的并发性也大大增加。但异步通信可能会引起Commit/Rollback指令丢失的问题,这就由消息中间件的超时询问机制来弥补。

消息中间件和下游系统之间为什么要采用同步通信呢?

异步能提升系统性能,但随之会增加系统复杂度;而同步虽然降低系统并发度,但实现成本较低。因此,在对并发度要求不是很高的情况下,或者服务器资源较为充裕的情况下,我们可以选择同步来降低系统的复杂度。 我们知道,消息中间件是一个独立于业务系统的第三方中间件,它不和任何业务系统产生直接的耦合,它也不和用户产生直接的关联,它一般部署在独立的服务器集群上,具有良好的可扩展性,所以不必太过于担心它的性能,如果处理速度无法满足我们的要求,可以增加机器来解决。而且,即使消息中间件处理速度有一定的延迟那也是可以接受的,因为前面所介绍的BASE理论就告诉我们了,我们追求的是最终一致性,而非实时一致性,因此消息中间件产生的时延导致事务短暂的不一致是可以接受的。

总结

  1. 以上实现方式,必须是消息中间件支持事务特性,否则无法实现;
  2. 分布式实现除了正常的成功、失败之外,一定要考虑消息超时情况;
  3. 生产者和MQ之间通过超时询问机制解决异常问题,MQ和消费者之间通过超时重试机制、定期校对机制解决异常问题,若最后都不能结决问题,就需要人工干预,例如消息标记为处理失败,然后发短信、邮件通知开发,因此,分布式系统设计不是什么问题都可以全部解决;
  4. 明白生产者、MQ、消费者 同步和异步的优点和劣势;

方案二:非事务消息中间件

此方式不支持消息的回滚,只保障最大程度的把消息发送成功。

image

  1. 上游系统操作成功之后(这和方案二是不同的),往本地消息表插入一条数据,即把任务处理过程和本地消息表插入信息 这两个步骤放到一个本地事务里面完成;如果向本地消息表插入消息失败,那么就会触发回滚,之前的任务处理结果就会被取消。如果这量步都执行成功,那么该本地事务就完成了。接下来会有一个专门的消息发送者不断地发送本地消息表中的消息,如果发送失败它会返回重试。当然,也要给消息发送者设置重试的上限,一般而言,达到重试上限仍然发送失败,那就意味着消息中间件出现严重的问题,此时也只有人工干预才能解决问题,如果发送成功,就如方案一一样,上游就开始处理其他的任务,自己的任务已经完成,不再关心这个消息被谁消费或者是否消费成功;这种方案和猎趣的JOB处理类似,相比第二种中方案,他达到数据一致性的周期比较长,而且还上游系统实现消息重发机制,以确保消息被成功的发送到消息中间件,这无疑增加了业务系统的开发成本,是的业务系统不够纯粹,作为业务系统来说就是发个消息而已,不应该在让其承担消息可靠性的保障,并且这额外的业务逻辑无疑会占用系统的硬件资源,影响性能,因为是任务轮询来发送消息。
  2. 下游系统:从消息中间件消费消息,处理逻辑和方案二的步骤五、六类似,即通过超时重试机制+定期校对机制来最大努力的保障消息被消费;

方案三:TCC(两阶段型、补偿性)

TCC即为Try Confirm Cancel,它属于补偿型分布式事务。顾名思义,TCC实现分布式事务一共有三个步骤:try:尝试执行业务; confirm:执行业务 ; Cancel:取消执行的业务。下面介绍两种场景例子

场景一:银行转账

假设用户A用他的账户余额给用户B发一个100元的红包,并且余额系统和红包系统是两个独立的系统。

  • Try:尝试待执行的业务,这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源
  1. 创建一条转账流水,并将流水的状态设为交易中
  2. 将用户A的账户中扣除100元(预留业务资源,很像猎趣账务系统转账,先冻结转账金额
  3. Try成功之后,便进入Confirm阶段
  4. Try过程发生任何异常,均进入Cancel阶段
  • Confirm : 这个过程真正开始执行业务,由于Try阶段已经完成了一致性检查,因此本过程直接执行,而不做任何检查。并且在执行的过程中,会使用到Try阶段预留的业务资源。
  1. 向B用户的红包账户中增加100元
  2. 将流水的状态设为交易已完成
  3. Confirm过程发生任何异常,均进入Cancel阶段
  4. Confirm过程执行成功,则该事务结束
  • Cancel:取消执行的业务,若业务执行失败,则进入Cancel阶段,它会释放所有占用的业务资源,并回滚Confirm阶段执行的操作
  1. 将用户A的账户增加100元
  2. 将流水的状态设为交易失败

场景二:下单扣库存

  1. Try阶段会去扣库存;
  2. Confirm阶段则是去更新订单状态;
  3. 如果更新订单失败,则进入Cancel阶段,会去恢复库存;
  4. 此处有个延伸,若恢复库存失败,怎么处理?可以参考方案二的技术 重试机制+定期校对+人工干预,具体实现方案可把失败消息存储到本地数据库,然后异步任务来重试;

总结

方案一和方案二

  • 相同点
  1. 都是基于消息中间件实现的;
  2. 上游系统只保证把消息成功发送到中间件,不关心下游系统消费情况,然后就去处理其他任务;也就是所谓的Base设计思想最终一致性,下游系统过一段时间之后才消费完任务;
  3. 下游若是处理失败,不支持上游系统的消息回滚,优点是提供系统的并发度;
  4. 使用场景例如支付成功通知订单系统等;

方案三和前两种区别

  1. 最大的区别就是方案三在第二部处理失败之后,支持第一步的处理回滚;
  2. 这种方式虽然能保证消息回滚,但是弊端就是并发力度无法保障;

遗留问题

2PC即两阶段提交算不算分布式事务的处理方案呢?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值