建议收藏随时翻看~
0
前言
这篇文章想从数据库比较难搞的“事务”着手,分析为什么要有事务,事务有哪些特性以及如何保证事务的这些特性。再由事务的特性中一致性来分析在分布式数据库中,分布式事务的一致性这个更加棘手的问题,并介绍分布式事务是如何解决这一难题的。进一步扩展,分布式的CAP理论中的C(Consistency)也提到了一致性这一概念,跟数据库中事务的一致性又有什么联系呢。通过这一串的分析,希望能帮大家厘清这里面的模糊概念。
1
单机事务及一致性
首先这里先限定为单机数据库,事务是数据库操作执行的单位,如果我们提交一个SQL命令,那么每个查询或者数据库更新语句就是一个事务。如果我们使用嵌入式SQL接口,那么事务的范围就可以通过编程来控制,可能包含一条或者多条SQL语句。
如果一个数据库声称自己支持事务,那么他必须满足四个特性:A(原子性)C(一致性)I(隔离性)D(持久性)。
原子性:原子性就是指一个事务的执行要么全部成功,要么全部失败,全部失败的意思是指即使有大部分成功了,只要有一小部分失败就要全部失败回滚。
一致性:一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,这听起来很抽象,大部分的解释都是拿一个银行转账的例子来进行说明:比如小舰和小花各有500块钱,加起来是1000块钱;小舰给小花转了50之后,小舰变成了450,小花变成了550,他们加起来的总和还是1000块钱,如果出现小舰的账户减了50,小花的账户没有变化,那就出现了不一致的问题了。
隔离性:隔离性是指多个用户并发访问数据库的时候,不同用户的事务操作之间不能互相影响,要保持互相独立性,不然可能就会出现我改了你的,你改了他的,他读了我的,可以想象这种情况下,大家修改和读取到的数据还靠谱么??讲到这里,可能自然而然就会联想到事务的几种隔离级别,没错,事务的隔离级别就是来应对刚刚讲到的【我改了你的,你改了他的,他读了我的】这种混乱的局面,稍后我们详谈。
持久性:持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。那么数据库要通过什么手段来进行这样的保证呢?答案就是日志!对于数据库及各种(大)数据管理系统来讲的重要组成部分,主要的日志有redu和undo两种,undo和redo在恢复时会撤销所有未提交的事务,重做已提交的事务。
我们从最简单的开始说起。
持久性:就拿刚刚讲到的持久性开始,上面说到持久性是通过redu和undo日志来保证的,但是具体是怎么做的呢?
如果某个事务修改一条记录,在进行修改之前,需要将旧值记录到undo log中,在进行修改操作后,会将新值写入redo log中,在事务修改提交之后,会将提交状态写入undo log和redo log中,需要注意的是,commit状态写入undo log是在更新数据落盘之后,commit状态写入redo log是在更新数据落盘之前。这样在进行数据恢复时,对于已经COMMIT的事务使用redo log进行重做,对于没有COMMIT的事务,使用undo log进行回滚。
原子性和隔离性:这两个特性其实就是为了让事务能够并发执行,在数据库中是通过并发控制管理器或调度器来保障的。
那么,不同事务之间的并发执行是否要真的做到互不影响呢?其实,在实际的数据库实现过程中,为了兼顾一致性和性能,划分了不同的隔离级别,不同的隔离级别对于隔离性(或者说一致性)有不同的约束。隔离级别越低,可能并发性越好,但是事务之间可能就会互相影响;但是如果隔离级别太高,事务之间隔离了,但是并发性以及性能又下去了。因此,不同的数据库会针对不同的场景和性能要求权衡之后采用适当的隔离级别。
数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。这四种不同的隔离级别对应着在事务的并发操作中可能会出现的四种不同的不一致性问题:脏读,不可重复读,幻读。
一致性:一致性放在最后是由原因的,一致性是指数据处于一种语义上的有意义且正确的状态。一致性是对数据可见性的约束,正如上面提到的,不同的隔离级别可能会出现不同的一致性问题,因此不同系统或场景对一致性的要求也是不一样的。当然,在数据库事务的ACID中,一致性指的是强一致性,但是在分布式的CAP理论中,这个一致性可能还包括弱一致性、最终一致性等类型。
再回来说,一致性其实是与持久性、原子性和隔离性都有关系的,通过原子性保证事务要么全部执行成功、要么全部执行失败,这其实是一致性的一个根本保证,通过隔离级别保证并发事务之间的数据可见性,从而划分了不同级别的一致性,也可以说隔离级别破坏了一致性,因此在数据库系统实现的时候要在它们之间当然要做一个权衡,持久化存储当然是一致性的最终保障措施,当故障发生的时候,通过日志进行回滚或重做,保证操作结果不丢失。
2
分布式事务一致性
在说分布式事务之前,首先要说一下数据分布,准确的说是分布式的分布。分布式分布的一个重要原因是组织机构自身分布在若干个节点。例如银行中,尽管用户的某个信息表在逻辑上只有一张表,但是在物理上,在各个分行的数据库中都有一份。
数据分布的结果导致一个事务可能涉及多个节点的处理。因此需要重新构建一个事务处理模型。
上面提到的,涉及到多个节点的数据处理,我们之前的单机事务模式可能就不合适了,比如我们要怎么管理分布事务的提交或者终止回滚?
考虑这样一个常见的场景:在某个银行转账场景中,小舰和小花的账户数据分别在A节点和B节点的数据库中,如果小舰想给小花转账50元,那么应该在A节点的数据库中给小舰的账户余额减去50,然后再B节点的数据库中给小花的账户余额加上50,这样就保证了数据的一致性。但是如果在A节点上执行的扣款操作成功,而在B节点上的充值操作失败,亦或者在B节点充值成功,A节点由于余额不足无法完成扣款导致扣款失败,这都会导致出现数据不一致的问题。
分布式数据库决定是否提交一个分布式事务可能使用很复杂的协议,但是它们都基于两阶段提交的基本思想。
两阶段提交:2PC(Two-phase commit protocol),两阶段提交是一种强一致性设计,2PC 引入一个事务协调器的角色来协调管理各节点(也可称之为参与者或各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。我们来看下两个阶段的具体流程。

图1 两阶段提交的第一阶段中的消息
提交请求(投票)阶段
协调器向所有节点发送prepare请求与事务内容,询问是否可以准备事务提交,并等待各个节点的响应。
节点执行事务中包含的操作,并记录undo日志(用于回滚)和redo日志(用于重放),但不真正提交。
节点向协调器返回事务操作的执行结果,执行成功返回ready,否则返回don't commit。

图2 两阶段提交的第二阶段中的消息
提交(执行)阶段
分为成功与失败两种情况。
若所有节点(参与者)都返回ready,说明事务可以提交:
协调器在其节点中记录<commit T>日志;
协调器向所有参与者发送commit请求。
各节点收到commit请求后,将事务真正地提交上去,并在此过程中记录日志<commit T>并释放占用的事务资源,并向协调器返回ack。
协调器收到所有节点的ack消息,事务成功提交完成。
若有一个或多个节点(参与者)返回don't commit 或者超时未返回,说明事务中断,需要回滚:
协调器在其节点中记录<abort T>日志记录
协调器向所有节点发送abort T消息
各节点收到abort T消息后,将各节点中止或者根据undo日志回滚到事务执行前的状态,并记录<abort T>日志,释放占用的事务资源,并向协调器返回ack。
协调器收到所有节点的ack消息,事务中止或回滚完成。
以上就是分布式事务采用的两阶段提交的思想,正如前面所讲到的,两阶段提交其实只是一个基本思想,除了2PC,还有3PC,TCC等协议,也是类似的套路,只是进一步的完善,或者扩大了适用范围。
3
分布式系统的一致性
上面无论是单机事务的一致性,还是分布式事务的一致性,可以发现都是针对数据库的事务而言的,然而分布式系统的一致性是一个更加多元和复杂的场景,单纯的2PC或者3PC协议无法满足分布式系统中的其他一致性需求。
CAP理论是分布式系统、特别是分布式存储领域中被讨论的最多的理论。其中C代表一致性 (Consistency),A代表可用性 (Availability),P代表分区容错性 (Partition tolerance)。CAP理论告诉我们C、A、P三者不能同时满足,最多只能满足其中两个。
在CAP理论中,P是一定要满足的,那么也就意味着只有两种组合方案(AP和CP)进行选择,但是在实际中,面对鱼和熊掌不可兼得的情况下,我们还是想想方设法兼得的,也就是C和A各退一步,我们来保证系统的基本可用和最终一致性。
讨论分布式系统的一致性涉及到更多的一个概念可能是共识。举个最简单的例子就是在一个或多个进程提议了一个值后,通过某种协议让所有进程对这个值达成共识(一致),为了就某个值达成共识,各个进程需要提出自己的提议,最终通过分布式一致性算法,使得所有正确运行的进程学习到相同的值。
分布式一致性算法我们应该已经听过很多了,例如Paxos、Raft以及Zab协议等。Paxos算法是布式系统中通用的一致性方案中的一种,它能达到某种最终一致性的状态。Raft,不同于Paxos直接从分布式一致性问题出发推导出来,Raft则是从多副本状态机的角度提出,使用更强的假设来减少需要考虑的状态,使之变的易于理解和实现。Zab协议 的全称是Zookeeper Atomic Broadcast(Zookeeper原子广播)。Zookeeper 是通过 Zab 协议来保证分布式事务的最终一致性。
4
总结
讲了这么多,我们再来看下前言提到的目标是否完成了。本篇文章从单机数据库的事务着手,明白了事务是数据库操作执行的单位,它包含ACID四个特性,D(持久性)是由日志来保证的;原子性和隔离性是通过并发控制管理器或调度器来保障的,其中隔离性又涉及到了四种不同的隔离级别(读未提交、读已提交、可重复读和可串行化);一致性其实是一个比较复杂的概念,它与其他的三个特性都有关系。但总体来讲,单机数据库事务还是比较简单的,各个特性都比较容易保障。到了分布式数据库中,情况就要复杂一点了,一个全局事务由多个节点的单机事务组成,如何去管理分布式事务的提交或者中止,如何进行故障故障恢复,要解决这些问题,就需要重新构建一个事务处理模型。解决分布式事务处理的一个基本思想就是2PC,第一阶段进行提交请求阶段,第二阶段是提交执行阶段。以2PC为基础,又衍生出了3PC、TCC等协议。如果说在数据库中,一致性更多强调的是事务的一致性特性,在分布式系统中,一致性的概念重点在于共识。所谓的共识就是多个不同的节点进程之间就某个值达成一致。我们常听到的分布式一致性算法(Paxos、Raft)就是为了促进各个节点达成一致而生的。


长按二维码识别关注

文章都看完了
不点个
吗
欢迎 点赞、在看、分享 三连哦~~




