聊一聊分布式事务那些事儿

Posted 腾讯在线教育技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊一聊分布式事务那些事儿相关的知识,希望对你有一定的参考价值。

  让我们以一个实际的业务问题开始,具体业务场景如下:比如在支付系统中用户的一次支付操作,支付完成以后后台需要同时调用服务 A、B、C,要满足几个调用要么同时成功;要么同时失败。A、B、C几个服务 可能是多个不同部门开发、部署在不同服务器上的远程服务。
  更普遍的是在分布式服务中,每次请求一般都由多个独立服务相互调用,在某些业务场景中必须保证数据的一致性,那如何解决分布式调用数据的一致性呢?

事务

  事务,又叫数据库本地事务,估计很多人和我一样第一次听说事务是在学习关系型数据库的时候,所谓事务是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行,一系列数据库操作要想成为事务必须要满足ACID特性。所以事务就是一种“要么全部执行成功做,要么完全执行(All or Nothing)”机制。

ACID

  ●A:原子性(Atomicity),一个事务中的所有操作,要么完全地执行,要么完全地不执行,如果中间某一操作失败全部回滚到事务开始之前的状态。在实现上数据库的原子性是通过 Undo Log来实现。简单来说就是在操作任何数据之前,首先将数据备份到Undo Log,然后进行数据的修改。如果出现了错误或者用户执行了Rollback语句,可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。
   ●C:一致性(Consistency),指数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
   ●I:隔离性(Isolation),是针对多个事务而言的,当不同的事务同时操纵相同的数据时,每个事务都有自己的空间,都是隔离的。在实现上,事务的隔离性是通过数据库锁的机制实现的。至于具体的隔离级别和实现这里就不详细讨论了。
  ●D:持久性(Durability),是指只要事务成功,它对数据库所做的操作就必须永久保存下来。即使数据库宕机崩溃,重新启动后,仍然能恢复到事务成功结束时的状态。在实现上,数据库主要是通过Redo Log(重做日志)机制实现持久性,具体的来说就是在事务提交前,只要将更新操作记录在Redo Log,然后Redo Log持久化即可,不需要将数据持久化。当系统崩溃时重启后可以根据 Redo Log 内容,将所有数据恢复到最新的状态。

分布式理论

CAP理论

  ●C: 一致性(Consistency),是指在分布式系统中通过某个节点的写操作结果经过一定的时间后通过其他节点读操作可见,这里根据写操作到读可见的时间间隔,可以将一致性分为3种类型:强一致性:更新操作后,并发访问情况下可立即感知该更新。最终一致性:更新操作后,在一定的时间后可感知该更新。弱一致性:更新操作后,允许部分或全部节点无法感知该更新。
  ●A: 可用性(Availability),指一个系统必须在有限的时间返回合理地结果
  ●P:分区容错性(Partition tolerance ):当分布式系统某些节点出现问题后,系统能够继续服务。
  CAP定理指出一个软件系统无法同时满足一下C,A,P 3个特性。对于传统关系数据库系统来说由于是单机系统,其无法满足 Partition (分区容错性),所以可以满足CA。在分布式系统中网络分区是一个天然的属性,分区是一个必然结果,所以必须在C(一致性)和A(可用性)二者之间抉择。
   举例来说:ZooKeeper是一个分布式协调服务,它的职责是保证数据(配置数据,状态数据)在其管辖下的所有服务之间保持同步、一致,因此必须保证一致性牺牲可用性,因此ZooKeeper是个CP系统,即任何时刻对ZooKeeper的访问请求能得到一致的数据结果,同时系统对网络分割具备容错性;但是它不能保证每次服务请求的可用性(注:也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果),zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,选举期间整个zk集群都是不可用的。Eureka作为服务注册中心,在设计时就优先保证可用性。,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。

BASE理论

   在分布式系统中,服务的可用性是一个系统最重要的指标,它的重要程度比一致性要高,那么如何实现高可用性呢? 根据CAP理论,要满足高可用性必须要牺牲一致性,于是有人针对这种情况提出了另外一个理论,就是BASE理。BASE理论指的是:
 ● Basically Available(基本可用)
 ● Soft state(软状态)
 ● Eventually consistent(最终一致性)
 理论的核心思想就是:我们无法做到强一致,允许数据在一段时间内是不一致的,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency),BASE理论是对CAP中的一致性和可用性进行一个权衡的结果。

分布式事务

   本地事务由数据库的本地资源管理器进行管理的,对于分布式数据库或者分布式系统来说传统的本地事务已经无法处理。分布式事务用于在分布式系统中保证不同节点之间的数据一致性。
   分布式事务的解决方案有很多种,最具有代表性的是XA分布式事务协议。XA协议包含两阶段提交(2PC)和三阶段提交(3PC)两种实现。此外还有TCC,本地消息表,消息事务等一系列解决方案。

1 两阶段提交

   两阶段提交(2PC)是由协调者和参与者组成,一共经过两个阶段和三个操作,流程图如下:

聊一聊分布式事务那些事儿

                                                        [ 2PC ]


 1)第一阶段:准备阶段(prepare),协调者通知参与者准备,参与者完成准备工作向协调者回应ready。
 2)第二阶段:提交(commit)/回滚(rollback)阶段,协调者根据参与者的回应发起最终的提交指令,如果有参与者没有准备好则发起回滚指令。
优点:
  实现比较简单,尽量保证了数据的强一致性,适合对数据强一致要求很高的关键领域。
缺点:
   ●单点问题:事务协调者在整个流程中是单点,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,事务参与者就会一直阻塞。
   ● 同步阻塞:在准备就绪之后,事务参与者的资源一直处于阻塞,直到提交完成,释放资源。
   ● 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。比如在第二阶段中,假设协调者发出了事务Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
   ● 性能问题:两阶段提交协议执行过程中,所有参与节点都是事务阻塞型的,性能低下。


2 三阶段提交

   为了解决两阶段提交协议的阻塞问题提出了 三阶段提交协议(3PC),从原来的两个阶段扩展为三个阶段,增加了一个PreCommit阶段,并且增加了超时机制。

聊一聊分布式事务那些事儿

                                                [ 3PC ]


PreCommit阶段分两种情况:
  1、所有参与者均反馈YES,即执行事务预提交。
  2、任何一个参与者反馈NO,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务。
   3PC中一旦参与者迟迟没有接到协调者的commit请求,会自动进行本地commit,这样有效解决了协调者单点故障的问题。同时引入了超时机制,解决了在异常情况下2PC的阻塞问题,但导致一次提交要传递更多的消息,延时很大,并且性能问题和不一致的问题仍然没有根本解决。


3 TCC

   TCC是Try、Commit、Cancel三种指令的缩写,其逻辑类似于XA两阶段提交,但是实现方式是在代码层面来人为实现。TCC含义如下:
Try 检查及预留资源,完成提交事务前检查,并预留好资源。
Confirm 确定执行业务操作,对try阶段预留的资源正式执行。
Cancel 取消执行业务操作,对try阶段预留的资源释放。

聊一聊分布式事务那些事儿

                                                [ TCC ]


优点:
  最终保证数据的一致性,在业务层实现事务控制,灵活性好。TCC把资源层面二阶段提交提到了业务层面来实现,有效避免了XA两阶段提交占用资源锁时间过长导致的性能低下问题。
缺点:
  开发成本高,每个事务操作每个参与者都需要实现try/confirm/cancel三个接口。在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。最重要的是TCC可能会出现因超时导致的资源悬挂等问题。因此TCC适用与一些强隔离性,严格一致性要求的活动业务,执行时间较短的业务,业界内支付宝采用的这种方案。
  注意:TCC的try/confirm/cancel接口都要实现幂等性,在为在try、confirm、cancel失败后要不断重试 ;它让多个系统保证了原子性操作,因此成本还是比较高的。


4 本地消息表

   本地消息表其核心思想是将分布式事务拆分成本地事务进行处理,解决方案如下图所示:

聊一聊分布式事务那些事儿

                                                [ 本地消息表 ]


   消息生产方新增一个消息表,用于记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
  消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
  生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
优点:
   一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点:
  消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。需要设计DB消息表,同时还需要一个后台任务,不断扫描本地消息。导致消息的处理和业务逻辑耦合额外增加业务方的负担。
  本地消息表遵循BASE理论,采用的是最终一致性模型,适用于对一致性要求不高的情况。实现这个模型时需要注意重试的幂等。


5 消息事务

  所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败。在某些消息中间件如RocketMQ的实现中,为了能解决发送消息和更新DB的一致性问题,同时又不和业务耦合,RocketMQ提出了“事务消息”的概念,实际就是将本地消息表移动到了 MQ 内部,从而实现了分布式事务,实现系统的最终一致性。。也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
  基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ比如 RabbitMQ 和 Kafka 都不支持,RocketMQ事务消息部分代码也未开源。

6 一致性校验

   一致性方案是教育支付后台使用的一种方案,支付发货是通过消息队列解藕,然后通过一致性服务后台确保数据的最终一致性。一致性服务会通过同时订阅发货消息和确认消息来校验是否一个发货消息对应的消费者是否所有服务都确认消息,如果有服务未确认消息说明这个消息消费失败需要重放消息,这里最重要的是要保证消息的消费者业务是幂等性操作,这样消息重放才会对已消费服务无影响。

                                            [ 一致性校验 ]


幂等性:

   所谓的幂等性是指同一个操作无论请求多少次,其造成的结果都是相同。在实现上主要有以下几种方式:
  第一种方式:执行操作之前判断是否已经执行,如果执行过了就不再执行。课堂业务中用户中心的关系链就是使用这种方法,添加关系链之前,取出关系链判断是否已经存在报名关系,如果存在直接返回,否则添加关系。
  第二种方式:在数据库表中加一个状态字段(未处理,已处理),数据操作时判断未处理时再处理。课堂业务中订单中心使用的这种方式,发货修改订单如果订单状态是已发货直接返回,否者修改订单状态。
一致性方案选型对比如下:

                                        [ 一致性方案对比 ]


以上是关于聊一聊分布式事务那些事儿的主要内容,如果未能解决你的问题,请参考以下文章

聊一聊DTM子事务屏障功能之SQL Server版

聊一聊如何用C#轻松完成一个SAGA分布式事务

微服务架构下分布式事务的那些事儿

分布式事务:三个概念

分布式系统的那些事儿 - SOA架构体系

分布式事务开局第一篇,从数据库事务隔离级别说起