分布式事务全攻略

Posted csc20190701

tags:

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

1.什么是分布式事务

1.1.本地事务

  本地事务,是传统的的单机数据库事务,必须具备ACID原则;

 

  • 原子性(A)

 

  在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。

  • 一致性(C)

  事务执行必须保证系统的一致性,在事务开始之前和事务结束之后,数据库的完整性没有被破坏,就拿转账为例,A有500元,B有500元,如果在一个事务里A成功转给B50元,那么不管发生什么,最后A账户和B账户的数据之和必须是1000元。

  • 隔离性(I)

  事务和事务之间不会相互影响,一个事务的中间状态不会被其他事务感知。数据库保证隔离性包括四种不同的隔离级别:

  • Read Uncommitted(读取未提交内容)
  • Read Committed(读取提交内容)
  • Repeatable Read(可重读)
  • Serializable(可串行化)
  • 持久性(D)

  一旦事务提交了,那么事务对数据做的变更就完全保存在数据库中,即使发生停电,系统宕机也是如此。

 

  因为在传统项目中,项目部署基本是但单点式:即单个服务和单个数据库。这种情况下,数据库本身的事务机制就能保证ACID的原则,这样的事务都是本地事务。

 

  概括来讲,单个服务与单个数据库的架构中,产生的事务都是本地事务。

 

  其中原子性和持久性就要靠undo和redo日志来实现。

1.2.undo和redo

  在数据库系统中,既有存放数据的文件,也有存放日志的文件。日志在内存中也是有缓存Log buffer,也有磁盘文件log file。

  mysql中的日志文件,有那么两种与事务有关:undo日志与redo日志。

1.2.1.undo日志

  数据库事务具备原子性(Atomicity),如果事务执行失败,需要把数据回滚。

  事务同时还具备持久性(Durability),事务对数据所做的变更就完全保存在了数据库,不能因为故障而丢失。

  原子性可以利用undo日志来实现。

  

  Undo Log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到Undo Log。然后进行数据的修改。如果出现错误或者用户执行rollback语句,系统可以利用Undo Log中的备份将

  数据恢复到事务开始之前的状态。

  数据库写入数据到磁盘前,会把数据先存在内存中,事务提交时才会写入磁盘中。

  用Undo Log实现原子性和持久化的事务简化过程:

  假设有A、B两个数据,值分别时1,2。

  1. 事务开始
  2. 记录A=1到undo log。
  3. 修改A=3
  4. 记录B=2到undo log
  5. 修改B=4
  6. 将undo log写到磁盘
  7. 将数据写到磁盘
  8. 提交事务
  • 如何保证持久性?

  事务提交前,会把修改数据到磁盘前,也就是说只要事务提交了,数据就肯定持久化了。

  • 如何保证原子性?

  每次对数据库修改,都会把修改前数据记录在undo log,那么需要回滚时,可以读undo log,恢复数据。

  若在7和8之间崩溃,此时事务未提交,需要回滚。而undo log已经被持久化,可以根据undo log恢复数据。

  若系统在7之前崩溃,此时数据并未持久化到硬盘,依然保持在事务之前的状态。

缺陷:每个事务提交前将数据和Undo Log写入磁盘,这样会导致大量的磁盘IO,因此性能很低。

 

  如果能够将数据缓存一段时间,就能减少IO提高性能。但是这样就会丧失事务的持久性。因此引入了另外一种机制来实现持久化,即Redo Log。

 1.2.2.redo日志

  和Undo Log相反,Redo Log记录的是新数据的备份。在事务提交前,只要将Redo Log持久化即可,不需要将数据持久化,减少了IO的次数。

  先看下基本原理:

  Undo + Redo事务的简化过程

  假设有A、B两个数据,值分别为1,2

  1. 事务开始
  2. 记录A=1到undo log buffer
  3. 修改A=3
  4. 记录A=3到redo log buffer
  5. 记录B=2到undo log buffer
  6. 修改B=4
  7. 记录B=4到redo log buffer
  8. 将undo log写入磁盘
  9. 将redo log写入磁盘
  10. 事务提交

  安全和性能问题

  • 如何保证原子性?

  如果在事务提交前故障,通过undo log日志恢复数据,如果undo log都还没写入,那么数据就尚未持久化,无需回滚。

  • 如何保证持久化?

  因为数据已经写入redo log ,而redo log已经持久化到了硬盘,因此只要到了步骤9以后,事务是可以提交的。

  • 内存中的数据库数据何时持久化到硬盘?

  因为redo log 已经持久化,因此数据库数据写入磁盘与否影响不大,不过为了避免出现脏数据(内存中与磁盘不一致),事务提交以后也会将内存数据刷入磁盘(也可以按照固定的频率刷新内存数据到磁盘中)

  • redo log何时写入磁盘

  redo log会在事务提交之前,或者redo log buffer 满了的时候写入磁盘。

  • 性能是如何提高的?

  将数据库数据写入磁盘是随机写(在磁盘上先寻址,再写入,寻址往往耗费更多的时间),将redo log写入磁盘写入磁盘是顺序写(在磁盘上开辟一条连续的空间写入数据,减少了寻址所需的消耗)。实际上undo log并不是直接写入磁盘,而是先写入到redo log buffer中,当redo log持久化时,undo log就同时持久化到硬盘了。

  因此事务提交前,只需要对redo log持久化即可。

  另外,redo log并不是写一次就持久化一次,redo log在内存中也有自己的缓冲池:redo log buffer,每次写redo log 都是写入到buffer,在提交时一次性持久化到磁盘,减少IO次数。

  • redo log中记录的数据,有可能包含尚未提交事务,如果此时数据库崩溃,那么如何完成数据恢复?

  数据恢复有两种策略:

  恢复时,只重做已经提交了的事务

  恢复时,重做所有事务包括为提交和回滚了的事务。然后通过Undo Log回滚那些未提交的事务

  Inodb引擎采用的是第二种方案,因此undo log要在redo log前持久化

1.2.3.总结

  • undo log 记录更新前数据,用于保证事务原子性
  • redo log记录更新后数据,用于保证事务持久性
  • redo log有自己的内存buffer,先写入到buffer,事务提交时写入磁盘
  • redo log 持久化之后,意味着事务是可提交的

1.3.分布式事务

  分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务:

  • 跨数据源的分布式事务
  • 跨服务的分布式事务
  • 综合情况

  1)跨数据源

  随着业务数据规模的快速发展,数据量越来越大,单库表逐渐成为瓶颈。所以我们对数据库进行水平拆分,将原单库表拆分成数据库分片,于是就产生了跨数据事务问题。

技术图片

   2)跨服务

   在业务初期,“一块大饼”的单业务系统架构,能满足基本业务需求。但是随着业务的快速发展,系统的访问量和业务复杂程度在快速增长,但系统架构逐渐成为业务发展瓶颈,解决业务系统的高耦合/可伸缩问题的需求越来越强烈。

   如图所示,按照面向服务(SOA)的架构设计原则,将单业务系统拆分成多个业务系统,降低了各系统之间的耦合度,使不同的业务系统专注于自身业务,更有利于业务的发展和系统的容量的伸缩。

技术图片

 

   3)分布式系统的数据一致性问题

   在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。z在分布式网络环境下,我们无法保障所有服务、数据库都百分百可用一定会出现部分服务/数据库执行成功,另一部分执行失败的问题。

  当出现部分业务操作成功、部分业务操作失败时,业务数据就会出现不一致。

  例如电商行业中比较常见的下单付款案例,包括下面几个行为:

  • 创建订单
  • 扣减商品库存
  • 从用户账户余额扣款

  完成上面的操作要访问三个不同的微服务和三个不同的数据库。

技术图片

 

   在分布式环境下,肯定会出现部分操作成功,部分操作失败的问题,比如:订单生成了,库存也扣减了,但是用户余额不足,这就造成了数据不一致

  订单的创建、库存的扣减、账户扣款在每一个服务和一个数据库内是一个本地事务,可用保证ACID原则。

  但是当我们把三件事情看做一个事情时,要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许部分成功部分失败的现象出现,这就是分布式系统下的事务。

  此时ACID难以满足,这是分布式 事务要解决的问题。

2.解决分布式事务的思路

  为社么分布式系统下,事务的ACID原则难以满足?

  这得从CAP定理和BASE理论说起。

2.1.CAP定理

  本节内容参考:阮一峰的博客CAP定理的含义

2.2.Base理论

     BASE时三个单词的缩写:

  • Basically Available(基本可用)
  • Soft state(软状态)
  • Eventually consistent(最终一致性)

 

  而我们解决分布式事务,就是根据上述理论来实现。

  还是以上面的下单减库存和扣款为例:

  订单服务、库存服务、用户服务及他们对应的数据库就是分布式应用中的三个部分。

  ●CP方式:现在如果要满足事务的强一 致性,就必须在订单服务数据库锁定的同时,对库存服务、用户服务数据资源同时锁定。等待三个服务业务全部处理完成,才可以释放资源。此时如果有其他请求想要操作被锁定的资源就会被阻塞,这样就是满足了CP。

  这就是强一致,弱可用

  ●AP方式:三个服务的对应数据库各自独立执行自己的业务,执行本地事务,不要求互相锁定资源。但是这个中间状态下,我们去访问数据库,可能遇到数据不一致的情况, 不过我们需要做一些后补措施,保证在经过一-段时间后, 数据最终满足-致性。

  这就是高可用,但弱一致(最终一致)。

  由上面的两种思想,延伸出了很多的分布式事务解决方案:

  • XA
  • TCC
  • 可靠消息最终一致
  • TA

2.4.分阶段提交

2.4.1.DTP和XA

  分布式事务的解决手段之.一,就是两阶段提交协议 (2PC: Two-Phase Commit)

  那么到底什么是两阶段提交协议呢?

  1994年,X/Open组织(即现在的Open Group )定义了分布式事务处理的DTP模型。该模型包括这样,几个角色:

  ●应用程序( AP) :我们的微服务

  ●事务管理器( TM) ;全局事务管理者

  ●资源管理器(RM) : - -般是数据库

  ●通信资源管理器( CRM) :是TM和RM间的通信中间件

  在该模型中,一个分布式事务(全局事务)可以被拆分成许多个本地事务,运行在不同的AP和RM上。每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每一个本地事务都能同时成功,若有一个本地事务失败,则所有其它事务都必须回滚。但问题是,本地事务处理过程中,并不知道其它事务的运行状态。因此,就需要通过CRM来通知各个本地事务,同步事务执行的状态。

  因此,各个本地事务的通信必须有统- -的标准,否则不同数据库间就无法通信。XA就是X/Open DTP中通信中间件与TM间联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。

2.4.2.二阶段提交

   本节内容参考:漫话分布式系统共识协议: 2PC/3PC篇

2.4.3.使用场景

  对事务有强一致性要求,对事务执行效率不敏感,并且不希望有太多代码侵入。

 

2.5.TCC

  TCC模式可以解决2PC中的资源锁定和阻塞问题,减少资源锁定时间。

2.5.1.基本原理

  它本质是一种补偿的思路。事务运行过程包括三个方法,

  ●Try: 资源的检测和预留; 
  ●Confirm: 执行的业务操作提交;要求Try成功Confirm一定要能成功;
  ●Cancel: 预留资源释放。

  执行分两个阶段:

  准备阶段(try) :资源的检测和预留;

  ●执行阶段(confirm/cancel) :根据上一 步结果,判断下面的执行方法。如果上一步中所有事务参与者都成功,则这里执行confirm。 反之,执行cancel 

  技术图片

 

 

 

   粗看似乎与两阶段提交没什么区别,但其实差别很大: .

  ●try、 confirm、 cancel都是独立的事务,不受其它参与者的影响,不会阻塞等待它人
  ●try、 confirm、 cancel由程序员在业务层编写 ,锁粒度有代码控制 
 

2.5.2实例

   我们以之前的下单业务中的扣减余额为例来看下三个不同的方法要怎么编写,假设账户A原来余额是100,需要余额扣减30元。如图: 
技术图片

 

 

 

  ●一阶段(Try) : 余额检查,并冻结用户部分金额,此阶段执行完毕,事务已经提交

    。检查用户余额是否充足,如果充足,冻结部分余额

 

    。在账户表中添加冻结金额字段,值为30,余额不变

 

  ●二阶段

 

    。提交 (Confirm) :真正的扣款, 把冻结金额从余额中扣除, 冻结金额清空

      ■修改冻结金额为0,修改余额为100-30= 70元

 

    。补偿(Cancel) :释放之前冻结的金额,并非回滚

 

      ■余额不变,修改账户冻结金额为0

2.5.3.优势与缺点

   ●优势
    TCC执行的每一个阶段都会提交本地事务并释放锁, 并不需要等待其它事务的执行结果。而如果其它事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。

  ●缺点
    。代码侵入:需要人为编写代码实现try、confirm、 cancel, 代码侵入较多
    。开发成本高: 一个业务需要拆分成3个步骤,分别编写业务实现,业务编写比较复杂
    。安全性考虑: cancel动作如果执行失败,资源就无法释放,需要引入重试机制,而重试可能导致重复执行,还要考虑重试时的幂等问题 
 

2.5.4.使用场景

  ●对事务有-定的一致性要求(最终一 致)
  ●对性能要求较高
  ●开发人员具备较高的编码能力和幂等处理经验
 
 

2.6.可靠消息服务

  这种实现方式的思路,其实是源于ebay,其基本的设计思想是将远程分布式事务拆分成一系列的本地事务

2.6.1.基本原理

  一般分为事务的发起者A和事务的其它参与者B:

  ●事务发起者A执行本地事务

  ●事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B

  ●事务参与者B接收到消息后执行本地事务

  如图:

技术图片

 

 

  这个过程有点像你去学校食堂吃饭:

    ●拿着钱去收银处,点一份红烧牛肉面,付钱

    ●收银处给你发-个小票,还有一个号牌,你别把票弄丢了

    ●你凭小票和号牌一定能领到一份红烧牛肉面,不管需要多久

  几个注意事项:

    ●事务发起者A必须确保本地事务成功后,消息-定发送成功

    ●MQ必须保证消息正确投递和持久化保存

    ●事务参与者B必须确保消息最终一 定能消费, 如果失败需要多次重试

    ●事务B执行失败,会重试,但不会导致事务A回滚

 

2.6.2.本地消息表

   为了避免消息发送失败或丢失,我们可以把消息持久化到数据库中。实现时有简化版本和解耦合版本两种方式。

  1)简化版本

   原理图:

    技术图片

 

 

  ●事务发起者:

    。开启本地事务

    。执行事务相关业务

    。发送消息到MQ

    。把消息持久化到数据库,标记为已发送

    。提交本地事务

  ●事务接收者:

    。接收消息

    。开启本地事务

    。处理事务相关业务

    。修改数据库消息状态为已消费

    。提交本地事务

  ●额外的定时任务

    。定时扫描表中超时未消费消息,重新发送

 

   优点:

    ●与tcc相比,实现方式较为简单,开发成本低。

  缺点:

    ●数据一致性完全依赖于消息服务,因此消息服务必须是可靠的。
    ●需要处理被动业务方的幂等问题
    ●被动业务失败不会导致主动业务的回滚,而是重试被动的业务
    ●事务业务与消息发送业务耦合、业务数据与消息表要在一起

 

  2)独立消息服务

   为了解决.上述问题,我们会引入一个独立的消息服务,来完成对消息的持久化、发送、确认、失败重试等一系列行为, 大概的模型如下: 
技术图片

 

 

  一次消息发送的时序图:

技术图片

 

 

  事务发起者A的基本执行步骤:

    ●开启本地事务

    ●通知消息服务,准备发送消息(消息服务将消息持久化,标记为准备发送)

    ●执行本地业务,

      。执行失败则终止,通知消息服务,取消发送(消息服务修改订单状态)

      。执行成功则继续,通知消息服务,确认发送(消息服务发送消息、修改订单状态)

    ●提交本地事务

  消息服务本身提供下面的接口:

    ●准备发送:把消息持久化到数据库,并标记状态为准备发送

    ●取消发送:把数据库消息状态修改为取消

    ●确认发送:把数据库消息状态修改为确认发送。尝试发送消息,成功后修改状态为已发送

    ●确认消费:消费者已经接收并处理消息,把数据库消息状态修改为已消费

    ●定时任务:定时扫描数据库中状态为确认发送的消息,然后询问对应的事务发起者,事务业务执行是否成功,结果:

      。业务执行成功:尝试发送消息,成功后修改状态为已发送

      。业务执行失败:把数据库消息状态修改为取消

  事务参与者B的基本步骤:

    ●接收消息

    ●开启本地事务

    ●执行业务

    ●通知消息服务,消息已经接收和处理

    ●提交事务

 

   优点:

    ●解除了事务业务与消息相关业务的耦合
  
  缺点:

    ●实现起来比较复杂 
 

2.6.3.RocketMQ事务消息

  RocketMQ本身自带了事务消息,可以保证消息的可靠性,原理其实就是自带了本地消息表,与我们上面讲的思路类似。 

 

2.6.4.RabbitMQ的消息确认

   RabbitMQ确保消息不丢失的思路比较奇特,并没有使用传统的本地表,而是利用了消息的确认机制:

  ●生产者确认机制:确保消息从生产者到达MQ不会有问题

    。消息生产者发送消息到RabbitMQ时, 可以设置一个异步的监听器,监听来自MQ的ACK

    。MQ接收到消息后,会返回一个回执给生产者:

      ■消息到达交换机后路由失败,会返回失败ACK

      ■消息路由成功,持久化失败,会返回失败ACK

      ■消息路由成功,持久化成功,会返回成功ACK

    。生产者提前编写好不同回执的处理方式

      ■失败回执:等待一定时间后重新发送

      ■成功回执:记录日志等行为

  ●消费者确认机制:确保消息能够被消费者正确消费

    。消费者需要在监听队列的时候指定手动ACK模式

    。RabbitMQ把消息投递给消费者后,会等待消费者ACK,接收到ACK后才删除消息,如果没有接收到ACK消息会一直保留在服务端, 如果消费者断开连接或异常后,消息会投递给其它消费者。

    。消费者处理完消息,提交事务后,手动ACK。如果执行过程中抛出异常,则不会ACK,业务处理失败,等待下一条消息
 
  经过上面的两种确认机制,可以确保从消息生产者到消费者的消息安全,再结合生产者和消费者两端的本地事务,即可保证一个分布式事务的最终一 致性。
 

2.6.5.消息事务优缺点

   总结上面的几种模型,消息事务的优缺点如下:

    ●优点:

      。业务相对简单,不需要编写三个阶段业务

      。是多个本地事务的结合,因此资源锁定周期短,性能好

    ●缺点:

      。代码侵入

      。依赖于MQ的可靠性

      。消息发起者可以回滚,但是消息参与者无法引起事务回滚

      。事务时效性差,取决于MQ消息发送是否及时,还有消息参与者的执行情况
 
 

2.7.AT模式

  2019年1月份,Seata开源了AT模式。AT 模式是-种无侵入的分布式事务解决方案。可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题。

  
  在AT模式下,用户只需关注自己的“业务SQL”,用户的“业务SQL"作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

  可以参考Seata的官方文档。 
 
   优点:

    ●与2PC相比:每个分支事务都是独立提交,不互相等待,减少了资源锁定和阻塞时间
    ●与TCC相比:二阶段的执行操作全部自动化生成,无代码侵入,开发成本低

  缺点:

    ●与TCC相比, 需要动态生成二阶段的反向补偿操作,执行性能略低于TCC 

以上是关于分布式事务全攻略的主要内容,如果未能解决你的问题,请参考以下文章

深入分布式事务之多场景架构设计全攻略实践篇(建议收藏)

极客时间-左耳听风-程序员攻略-分布式架构入门

分布式事务——分布式事务简介分布式事务框架 Seata(AT模式Tcc模式Tcc Vs AT)分布式事务—MQ

90 SpringCloud 解决分布式事务--lcn解决分布式事务

86 SpringCloud解决分布式事务

MongoDB4.2分布式事务