微服务架构下分布式事务处理

数据库事务/本地事务/单机事务

主流关系型数据库都支持事务,数据库事务是我们实现本地事务的基础。事务有四大特性ACID:原子性,一致性,隔离性和持久性。其中原子性是这些特性的基础,我们希望构成事务的一系列操作要不就全部正常执行,要不就回滚全部不执行。

  • 原子性:事务是不可分割的单元,要不全部正确执行,要不全部不执行。
  • 一致性:事务开始前和结束后,系统的一致性约束条件不被破坏。
  • 隔离性:事务之间的执行相互独立不受影响,一个事务不会看到另一个事务还未提交的数据。
  • 持久性:事务提交之后,事务执行结果必须是持久化保存的。即使提交后系统发生崩溃,事务提交结果也不会丢失。

事务的四个特性中,原子性是最基础的要求,也是实现一致性的基础,通常为了实现一致性,可能会牺牲一些隔离性。主流数据库通常会完整支持数据库层面的事务原子性,一致性,持久性和一定限度的隔离性。

本地事务需要依托数据库事务,分布式事务需要依赖本地事务。实现分布式事务的一致性需要业务系统逻辑+事务原子性来保证,所以分布式事务的核心就是如何保证原子性,要不全部发生,要不全部不发生。举个例子:

一个转账系统,一致性要求是各账户总额这个约束性条件不能改变。A客户向B客户转账100元,我们需要在业务逻辑中将A账户扣减100元,B账户增加100(而不是50,如果是50即使保证了原子性,一致性约束也被破坏),所以只要保证AB客户账户的操作要不全部发生(而且都是100),要失败另一个操作也恢复到操作前状态。

分布式理论

一个分布式系统中,系统整体由各个分布式的系统组成和完成各项操作。分布式系统有一个重要理论或者原理:CAP定理:分布式系统中,无法同时最大限度的满足以下三个特性:

  • 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效),系统的约束性条件不会改变
  • 可用性(Availability) : 每个操作都必须以可预期的响应结束
  • 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成

因为系统各个组成部分出于复杂的网络和现实环境,风险是不可避免的,如果分区容错性都得不到保证,分布式系统将失去意义。所以如何协调一致性和可用性是分布式系统实现的一个关键,当前,可用性越来越成为一个系统的核心指标,在激烈竞争的互联网行业中,系统间歇不可用往往会给业务带来巨大挑战,因此我们往往需要牺牲一些一致性来换取系统的可用性和分区容忍性。

BASE理论描述了,我们“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性

BA:Basic Available 基本可用 整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:

  • 比如“一定时间”可以适当延长 当举行大促销时,响应时间可以适当延长
  • 比如给部分用户返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。

S:Soft State:柔性状态 同一数据的不同副本的状态,可以不需要实时一致。
E:Eventual Consisstency:最终一致性 同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

分布式事务解决方案

根据上面的描述,分布式系统通常有以下几种解决方案:

  • 基于数据库XA事务接口的全局事务,这是2PC协议的实现
  • 基于可靠消息队列
  • 本地消息表
  • TCC事务补偿机制

强一致性/全局事务/2PC

MySQL从5.5版本开始支持XA Transactions。XA 是一个两阶段提交协议,该协议分为以下两个阶段:

  • 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.
  • 第二阶段:事务协调器要求每个数据库提交数据。

其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。

image.png

在分布式系统中这个XA事务的具体实现还需要数据库上层的事务管理器来配合。它追求比较强的一致性,但是会带来可用性上缺陷。2PC在复杂调用链的微服务架构上可用性是非常差的。

基于可靠消息队列

在微服务架构中,分布式系统中各个部分处理不同的业务流程,A->B顺序处理两个业务流程,可靠消息队列保证A操作与消息通知B,要不全部发生,要不全部不发生。

image.png

以RocketMQ 中间件为例,其思路大致为:

  • 第一阶段Prepared消息,会拿到消息的地址。
  • 第二阶段执行本地事务
  • 第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
  • 第四阶段如果消息队列没有收到确认消息,会不断轮询业务系统的状态,然后更新消息队列中对应的消息

当然这里只是提供了消息的可靠性,即保证:如果收到消息A操作一定会完成,没有消息A操作一定会提交。但是仔细分析就能看到,AB两个系统并没有构成一个事务,消息发到B后,B一定能完成吗?B不能完成分为几种情况:

  • B服务超时:这种情况消息队列会不断重试,确保B服务给出明确响应。重试要求B服务保证幂等性。
  • B服务操作失败:人工干预(分析是系统异常还是本身一定完成不了),分情况是否需要给A系统发补偿消息

TCC事务补偿机制

TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  • Try 阶段主要是对业务系统做检测及资源预留
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

举个例子,假入 A 要向 B 转账100元,A和B的账户在两个不同的银行系统,思路大概是:
1、首先在 Try 阶段,A账户扣减100元(预留业务资源)
2、在 Confirm 阶段,执行远程调用的转账的操作,B账户增加100元。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel),将A账户恢复到扣减前余额。

TCC同样要求系统的幂等性,由于网络故障超时等情况,业务系统同样需要保证幂等性,以确保confirm和cancel的重复操作不会影响系统一致性。这里可以看到,其实还牺牲了一定的隔离性来保证一致性。

本地消息表

本地消息表是目前业界最常用的解决方案,其核心思想是将分布式事务拆分成本地事务进行处理。

image.png

基本思路就是:

消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。

消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

image.png
image.png
image.png

总结

分布式事务处理有两个关键的难点:

  1. 确保系统间消息的可靠
  2. 如何处理服务的超时,超时是一种不明确的状态,需要通过重试或人工干预处理,重试要求系统的幂等性
  3. 如何确定补偿操作,如果不能补偿怎么办

你可能感兴趣的:(微服务架构下分布式事务处理)