分布式事务原理与实践

 

  所谓事务,它是一个操作集合,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。比如网上订票,要么你定票成功,余票减一张; 要么你定票失败,余票的数量不变。这就要求购票和余票减少这两个不同的操作必须放在一起,成为一个完整的逻辑链,这样就构成了一个事务。

  事务说起来很简单,就是BEGIN TRAN,COMMIT TRAN,ROLLBACK TRAN 三个语句,但是事务的内部实现原理是非常复杂的,而分布式事务由于需要跨越多个服务器,操作多个数据库,复杂度比单机事务要复杂的多,本文尝试通过锁和并发控制的讲解,逐层抽丝剥茧,由浅及深将分布式事务的实现原理呈现给大家。

     

一、事务简介

     

1、事务的本质

  事务的核心是锁和并发,采用同步控制的方式保证并发的情况下性能尽可能高,且容易理解。

            

     

 

     

     

     

     

     

   

   

   

  计算机可以简单的理解为一个标准的打字机,尽管看起来计算机可以并行处理很多事情,但实际上每个CPU单位时间内只能做一件事,要么读取数据、要么计算数据、要么写入数据,所有的任务都可以看成这三件事的集合。

  计算机的这种特性引出了一个问题:当多个人去读、算、写操作时,如果不加访问控制,系统势必会产生冲突。而事务相当于在读、算、写操作之外增加了同步的模块,进而保证只有一个线程进入事务当中,而其他线程不会进入。这样的方法其实就是我们提的事务。

     

2、事务单元

  事务单元是通过Begin-Traction,然后Commit(Begin-Traction、Commit和Rollback之间所有针对数据的写入、读取的操作都应该添加同步访问),Begin和Commit之间就是一个同步的事务单元。例如,Bob给Smith 100块钱就是一个事务单元,这个过程中有很多步操作,具体如下图所示;但对业务来说,仅是一个转账的操作。

     

  当三个账户都在进行转账操作时,每个操作都涉及Smith账户,所有的事务都会排队,形成一组事务单元。
  事务单元之间的Happen-Before关系中的四种可能性:读写、写读、读读、写写。所有事务之间的关系都可以抽象成这四种之一,来对应现在所有的业务逻辑处理。在此基础之上,需要用最快的速度处理多个事务单元之间的关系,同时还能保障这四种操作的逻辑顺序。

     

     

     

     

     

3、两阶段锁协议

  Two Phase Lock(2PL)是数据库中非常重要的一个概念。数据库操作Insert、Update、Delete都是先读再写的操作。数据库利用这些操作的特性,在每一次查询过程中,只要查到数据,就会在该数据上加锁。理论上,所有被读取的数据都已加锁,不会再被其他人读到,也就是说对数据进行的中间操作状态对所有人都不可见,当所有中间状态完成后,提交操作时,解开锁,此时数据对所有系统可见,

  例如在转账过程中,所有人只能看到两种状态:开始时,A有钱,B没钱;结束时,B有钱,A没钱,而中间A减掉钱,B尚未加上钱的状态被锁隐藏掉了,这个操作就是数据库中处理事务的最标准的方式。如上图所示:事务中的Trx2(JoeLock)与其他事务不相关,因此可以并行执行;Trx1需要Lock两个数据Boblock和Smithlock,而Trx3同样需要Lock这两个数据,因此Trx3必须等待,且等待在Boblock上;Joe事务会先结束,Trx3会等到Trx1完成后才会开始。

     

 二、单机事务常见处理方法

单机事务的常见处理方法有排队法、排他锁、读写锁、MVCC等方式,下面来一一解析。

1、排队法

  事务处理中最重要也是最简单的方案是排队法,单线程地处理一堆数据。

在排队法中,在所有事务单元的所有的读事务,读写事务都是串行的方式来进行,这种方案的实现简单,但是并发性最差。

     

2、排它锁

  排它锁是针对同一个事务单元的数据进行访问控制。在上述讲解的排队法中,所有事务单元均以单线程的方式进行操作,但是有些场景不适合用单线程操作,可以利用排他锁的方式来快速隔离并发读写事务。数据库中有一些事务单元是共享的,如图中的事务单元1是共享的,事务单元2/3共享数据;针对事务单元2/3共享数据的所有读写Block住,事务单元1单独用一个锁来控制,用这种方式完成系统的访问控制。

     

     

     

3、读写锁

如果是一个只读的事务,例如只对数据进行查询操作,在该过程中数据一定不被修改,因此多个查询操作可以并行执行,因此一种针对读读场景的优化自然而然产生——读写锁。读写锁的核心是在多次读的操作中,同时允许多个读者来访问共享资源,提高并发性。

 

     

     

     

     

     

     

   

4、MVCC(多版本并发控制)

  MVCC的本质是Copy On Write,也就是每次写都是以重新开始一个新的版本的方式写入数据,因此,数据库中也就包含了之前的所有版本。在数据读的过程中,先申请一个版本号,如果该版本号小于正在写入的版本号,则数据一定可以查询到,无需等到新版本完全写完即可返回查询结果。这种方式可以在读读不阻塞的前提下,实现读写/写读不阻塞,尽可能保证所有的读操作并行,而写操作串行。

       

     

 

     

   

   

   

   

   

  MVCC的两种不同实现方式:

  第一种实现方式是将数据记录的多个版本保存在数据库中,当这些不同版本数据不再需要时,垃圾收集器回收这些记录。这个方式被PostgreSQL和Firebird/Interbase采用,SQL Server使用的类似机制,所不同的是旧版本数据不是保存在数据库中,而保存在不同于主数据库的另外一个数据库tempdb中/

  第二种实现方式只在数据库保存最新版本的数据,但是会在使用undo时动态重构旧版本数据,这种方式被Oracle和MySQL/InnoDB使用

     

三、分布式事务的处理方案

  分布式事务是指会涉及到操作多个数据库的事务。其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚),要想理解分布式事务,我们需要先介绍一下两阶段提交协议。

     

1、2PC(两阶段提交)

  两阶段提交协议(Two-phase Commit,2PC)经常被用来实现分布式事务。一般分为协调器和若干事务执行者两种角色。这里的事务执行者就是具体的数据库,抽象点可以说是可以控制给数据库的程序。 协调器可以和事务执行器在一台机器上。

  在分布式系统中,每个节点虽然可以知晓自己的操作的成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)。

     

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)

1.准备阶段:事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败,要么在本地执行事务,写本地的redo和undo日志,但不提交

2.提交阶段:如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)。

     

二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:

1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态

2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

由于二阶段提交存在着诸如同步阻塞、单点问题等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。

     

2、3PC(三阶段提交)

  3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit阶段:3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

PreCommit阶段:协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能:

  • 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
  • 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

doCommit阶段:该阶段进行真正的事务提交

     

     

 

     

     

     

     

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

     

   

原文地址:https://www.cnblogs.com/wangweitr/p/7901252.html