架构

【微服务】分布式事务介绍

什么是微服务架构?


简而言之,微服务架构的系统是一个分布式的系统,按业务进行划分为独立的服务单元,解决单体系统的不足,同时也满足越来越复杂的业务需求。每个微服务仅关注于完成一件任务并很好地完成该任务。

微服务架构的优势


  • 1. 将复杂的业务拆分成多个小的业务,每个业务拆分成一个服务,将复杂的问题简单化。利于分工,降低新人的学习成本。
  • 2. 微服务系统是分布式系统,业务与业务之间完全解耦,随着业务的增加可以根据业务再拆分,具有极强的横向扩展能力。
  • 3. 服务间采用 HTTP 协议通信,服务与服务之间完全独立。每个服务可以根据业务场景选取合适的编程语言和数据库。
  • 4. 服务独立部署,每个服务的修改和部署对其他服务没有影响。
  • 虽然微服务有以上的优势,但是微服务实践仍处于探索阶段,很多中小型互联网公司,鉴于经验、技术实力等问题,微服务落地比较困难。著名架构师Chris Richardson指出,目前微服务主要存如下几方面困难:
  • 1. 单体应用拆分为分布式系统后,进程间的通讯机制和故障处理措施变的更加复杂。
  • 2. 系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。
  • 3. 微服务数量众多,其测试、部署、监控等都变的更加困难。

随着RPC框架的成熟,第一个问题已经逐渐得到解决。例如Dubbo可以支持多种通讯协议,Spring Cloud可以非常好的支持restful调用。对于第三个问题,随着Docker、DevOps技术的发展以及各公有云PaaS平台自动化运维工具的推出,微服务的测试、部署与运维会变得越来越容易。

而对于第二个问题,现在还没有通用方案很好的解决微服务产生的事务问题。分布式事务已经成为微服务落地最大的阻碍,也是最具挑战性的一个技术难题。下面将深入和大家探讨微服务架构下,分布式事务的各种解决方案。

微服务架构下,如何克服分布式事务难题?


事务简介

事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组SQL语句组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。

  • 原子性(atomicity):个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
  • 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。
  • 隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性又分为四个级别:读未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable read,解决虚读)、串行化(serializable,解决幻读)。
  • 持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,及时不能都很好的满足,也要考虑支持到什么程度。

本地事务

大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)。本地事务的ACID特性是数据库直接提供支持。本地事务应用架构如下所示:

在JDBC编程中,我们通过java.sql.Connection对象来开启、关闭或者提交事务。代码如下所示:

onnection conn = ... //获取数据库连接
conn.setAutoCommit(false); //开启事务
try{
   //...执行增删改查sql
   conn.commit(); //提交事务
}catch (Exception e) {
  conn.rollback();//事务回滚
}finally{
   conn.close();//关闭链接
}

分布式事务典型场景


当下互联网发展如火如荼,绝大部分公司都进行了数据库拆分和服务化(SOA)。在这种情况下,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,用需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性。
典型的分布式事务场景:

  • 跨库事务

跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。笔者见过一个相对比较复杂的业务,一个业务中同时操作了9个库。下图演示了一个服务同时操作2个库的情况:

  • 分库分表

通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B拆分成了2个库:

对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对于sql:insert into user(id,name) values (1,“张三”),(2,“李四”)。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。

但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。

  • 服务化

微服务架构是目前一个比较一个比较火的概念。例如上面笔者提到的一个案例,某个应用同时操作了9个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立服务,以简化业务逻辑。拆分后,独立服务之间通过RPC框架来进行远程调用,实现彼此的通信。下图演示了一个3个服务之间彼此调用的架构:

Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。

小结:上述讨论的分布式事务场景中,无一例外的都直接或者间接的操作了多个数据库。如何保证事务的ACID特性,对于分布式事务实现方案而言,是非常大的挑战。同时,分布式事务实现方案还必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。

分布式事务典型场景:

银行转账业务是一个典型分布式事务场景,通常包括以下三种情况:

A. 支行内转账:同一银行的相同支行内转账

B. 行内转账:同一银行的不同支行间转账

C. 跨行转账:不同银行的系统进行转账

对于传统集中式架构,A、B通常为本地事务,C为分布式事务。业务微服务改造后,转入、转出通常为不同的微服务,同一个微服务也通常运行于不同实例中。A可能变成一个分布式事务,也可能通过一些方法规避,在本地事务内完成。B和C很难规避,只能是分布式事务。

微服务最佳实践建议尽量规避分布式事务,但是在很多业务场景(比如上面的B、C转账场景),分布式事务是一个绕不开的技术问题。

分布式事务常用解决方案


为了解决分布式系统一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中,最常用的是两阶提交协议(2 Phase Commitment Protocol)。

两阶段提交方案


2阶段提交是分布式事务传统解决方案,先进为止还广泛存在。当一个事务跨越多个节点时,为了保持事务ACID特性,需要引入一个作为协调者来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

交易中间件与数据库通过 XA 接口规范,使用两阶段提交来完成一个全局事务, XA 规范的基础是两阶段提交协议。

不仅要锁住参与者的所有资源,而且要锁住协调者资源,开销大。一句话总结就是:2PC效率很低,对高并发很不友好。

两阶段提交方案应用非常广泛,典型商用软件包括Oracle Tuxedo和IBM CICS。它的优点是对业务代码侵入较低,但缺点也很明显

引用《世界性难题...》一文原话 “国外具有几十年历史和技术沉淀的基于XA模型的商用分布式事务产品,在相同软硬件条件下,开启分布式事务后吞吐经常有数量级的下降。”

  • 性能低下:由于 XA 协议自身的特点,它会造成事务资源长时间得不到释放,锁定周期长,而且在应用层上面无法干预,数据并发冲突高的场景性能很差。
  • 单点问题:协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者所在服务器宕机,就会影响整个数据库集群的正常运行。比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态。
  • 同步阻塞:两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,效率及其低下。

因此,两阶段提交方案在互联网业务中很少使用,无法满足高并发需求。

为了这个弥补这种方案带来性能低的问题,大家又想出了很多种方案来解决,通过在应用层做文章,即入侵业务的方式,比较典型的是TCC 方案和基于可靠消息的最终一致性方案。

TCC事务方案(三阶段提交协议(Three-phase commit))


TCC事务模型在电商、金融领域落地较多。TCC方案其实是两阶段提交的一种改进。其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。基本原理如下图所示。

事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。

TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能,比如华为分布式事务中间件DTM性能极高,普通配置服务器可以支持全局事务1万+ TPS,分支事务3万+ TPS。 当然TCC方案也有不足之处,集中表现在以下两个方面:

业务侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。

实现难度较大。为了满足一致性的要求,要充分考虑幂等操作,允许重复执行,也要防止资源悬挂,做好并发访问控制和数据可见性控制等。

上述原因导致TCC方案大多被研发实力较强、有迫切需求的大公司所采用。微服务倡导服务的轻量化,而TCC方案中很多事务的处理逻辑需要应用自己编码实现,复杂且开发量大。

三阶段提交(3PC),是二阶段提交(2PC)的改进版本。
与两阶段提交不同的是,三阶段提交有两个改动点:

  • 1、引入超时机制。同时在协调者和参与者中都引入超时机制。
  • 2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

2PC与3PC的区别

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。所以在分布式场景中,往往我们说的只能是保证99.9%或者99.99%……这样的数据一致性。

柔性事务


所谓柔性事务是相对强制锁表的刚性事务而言。流程入下:服务器A的事务如果执行顺利,那么事务A就先行提交,如果事务B也执行顺利,则事务B也提交,整个事务就算完成。但是如果事务B执行失败,事务B本身回滚,这时事务A已经被提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作作反操作,恢复到未执行前事务A的状态。

缺点是业务侵入性太强,还要补偿操作,缺乏普遍性,没法大规模推广。

在基于补偿机制的分布式事务中,各个服务之间的操作是不可撤销的。当一个服务发生错误时,它会通过调用补偿服务的接口来执行相应的补偿操作,保证整个事务的一致性。

具体而言,当一个事务涉及到多个服务时,每个服务都需要实现相应的补偿操作。如果某个服务出现了错误,那么它会调用其他服务的补偿接口,让它们执行相应的补偿操作,以保证整个事务的一致性。与基于两阶段提交的分布式事务相比,基于补偿机制的分布式事务更加灵活,但同时也更加复杂,需要更多的开发工作。

基于消息的最终一致性方案


消息一致性方案是通过消息中间件保证上下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个本地事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。

消息最终一致方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性也很高,应用需要进行大量业务改造,成本非常高。

入侵代码的方案是基于现有情形“迫不得已”才推出的解决方案,实际上它们实现起来非常不优雅,比如TCC,一个事务的调用通常伴随而来的是对该事务接口增加一系列的反向操作,提交逻辑必然伴随着回滚的逻辑,这样的代码会使得项目非常臃肿,维护成本高。

针对上面所说的分布式事务解决方案的痛点,很显然,我们理想的分布式事务解决方案肯定是性能要好而且要对业务无侵入,业务层无需关心分布式事务机制的约束,做到事务与业务分离,也就是本文所重点推荐的非侵入事务。

消息最终一致性解决方案之RocketMQ 例子

目前基于消息队列的解决方案有阿里的RocketMQ,它实现了半消息的解决方案

第一阶段:上游应用执行业务并发送MQ消息

  • 上游应用发送待确认消息到可靠消息系统
  • 可靠消息系统保存待确认消息并返回
  • 上游应用执行本地业务
  • 上游应用通知可靠消息系统确认业务已执行并发送消息。

可靠消息系统修改消息状态为发送状态并将消息投递到 MQ 中间件

 第二阶段:下游应用监听 MQ 消息并执行业务

下游应用监听 MQ 消息并执行业务,并且将消息的消费结果通知可靠消息服务。

  • 下游应用监听 MQ 消息组件并获取消息
  • 下游应用根据 MQ 消息体信息处理本地业务
  • 下游应用向 MQ
  • 确认消息被消费
  • 下游应用通知可靠消息系统消息被成功消费,可靠消息将该消息状态更改为已完成