序
说到事务,我相信大家第一个想到的大概都是数据库事务,毕竟在我们做的项目中涉及到数据库的操作时,或多或少都要考虑数据异常时的数据回滚问题,所以往往也牵扯到了数据库事务的ACID
四个特性以及事务的回滚等问题。
今天我主要想从数据库事务入手,来聊聊分布式中的一些理论,以及分布式事务中常常出现的问题和解决方案,本篇文章主要作为理论概念性的普及,基本不会涉及到代码。
事务
事务(Transaction
)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元(Unit
),狭义上的事务特指数据库事务。
ACID
数据库事务具有四个特性,分别是原子性(Atomicity
)、一致性(Consistency
)、隔离性(Isolation
)和持久性(Durability
),简称为事务的ACID
特性。
原子性
原子性是指,事务的操作必须是一个原子的操作序列单元,这个事务是不可以再进行分割的,各项操作在事务的执行过程中要么全部成功,要么全部失败,不可能出现第三种状态。
也就是说,在一个事务内的任何操作失败,豆浆导致整个事务的失败,在这个操作执行之前的所有操作都将要进行回滚,只有当所有的操作全部都执行成功,这个事务才算执行成功。
一致性
一致性是一个十分重要的概念,在初学数据库的时候,我经常把原子性和一致性弄混,在事务的四个特性中我认为一致性是最重要的,毕竟所有的技术都是为了业务服务,如果业务数据出现不一致,那么存储的必要也不存在。
一致性是指事务的执行不会破坏数据库数据的完整性,一个事务在执行前和执行后,数据库都必须保持一致性状态。
上面的话比较难以理解,举个例子:银行转账时,从账户A
向账户B
转了1000
块,那么此时数据库层面来看,应该是账户A
减去1000
块,账户B
增加1000
块,不能出现其中一个账户的金额发生了变化,而另外一个账户金额却不发生变化的情况。这里的两个账户之间的状态转换就涉及到了数据一致性的问题。
隔离性
隔离性是数据库事务中另外一个比较难以记住的特性,事务的隔离性是指在并发环境中,并发的事务是相互隔离的,一个事务的执行不能被其他事务干扰。也就是说,不同事务的并发操作相同的数据时,每个事务都有各自完整的数据空间,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能相互干扰。
在标准的SQL
规范中,定义了四个隔离级别,不同隔离级别对事务的处理不同,四个隔离级别从低到高分别为:读未提交(Read Uncommited
)、读已提交(Read Commited
)、可重复读(Repeatable Read
)和串行读(Serializable
)。
四种隔离级别分别会遇到的问题,可以参考下面的表格,我们不展开说明。需要注意的是MySQL
在可重复读隔离级别下解决了幻读问题,具体原因可以参考其他资料自行学习,这里不展开。
持久性
事务的持久性也被称为永久性,是指事务一旦提交,它对数据库中对应数据的状态变更就应该是永久性的。换句话说,一旦某个事务成功结束,那么它对数据库所做的更新就必须被永久保存下来–即使发生系统崩溃或机器宕机等故障,只要数据库能重新启动,那么一定能将其恢复到事务成功结束时的状态。
分布式事务
随着分布式计算的发展,事务在分布式计算领域也得到了广泛的应用。在单机数据库中,我们很容易能够实现一套满足ACID
特性的事务处理系统,但在分布式数据库中,数据分散在各台不同的机器上,如何应对这些数据进行分布式的事务处理具有非常大的挑战。
分布式遇到的问题
通信异常
在集中式向分布式演变的过程中,必然会引入网络因素,而由于网络本身的不可靠性,因此也引入了额外的问题。分布式系统会需要在各结点之间进行网络通信,因此每次网络通信都会伴随着结点不可用的风险:网络问题、路由器甚至DNS
解析等等,都会导致分布式系统无法完成一次网络通信。
除此之外,即使是网络之间可以顺利进行通信,网络之间的延迟问题也需要考虑,在集中式系统中一个简单的请求可能仅仅耗时1
纳秒,到了分布式系统中可能都需要1
毫秒,同时网络导致的数据丢失和数据被篡改也会变得可能。
网络分区
当网络发生问题时,可能会导致原来处于同一个区域内的结点,被分成了多个区域,每一个区域内的结点之间可以顺利通信,但是不同区域之间的结点却无法进行通信,这样会导致部分结点可以提供服务,而另外一些结点却不能提供服务。这就是常见的网络分区问题,俗称脑裂。
在部分结点出现无法通信的时候,我们不能因为这些结点不可用而让整个系统停下来,所以就需要考虑在分布式条件下,当缺失这些结点时,服务依然可用。
三态
在集中式系统中,针对特定的请求处理结果一般就是成功或者失败,而在分布式系统中却出现了另外一种状态–超时(或者叫未知),在分布式系统中,会存在网络超时的问题,通常这些网络超时会出现以下两种情况:
- 由于网络原因,请求消息未被发送到接收者那边,导致消息丢失
- 请求消息顺利发送到了消息接收者那里,但是消息接收者在反馈的时候由于网络异常,反馈消息无法顺利反馈,导致消息丢失
CAP和BASE理论
在集中式事务处理系统中,我们可以通过采用已经被实践证明的成熟的ACID
模型保证数据严格一致性。而随着分布式系统的出现,传统针对单机系统的事务模型可能不再适用,特别是针对一个高访问量、高并发的互联网分布式系统来说,如果我们需要实现一套严格满足ACID
特性的分布式事务,很可能出现的情况就是在系统的可用性和严格一致性之间出现冲突–因为当我们要求分布式系统具有严格一致性时,很可能就需要牺牲掉系统的可用性。而这一点我们在上一小节中也提到过,系统可用性常常是一个消费者不允许我们讨价还价的系统属性。
因此,在系统的一致性和可用性之间永远也无法存在一个两全其美的方案,于是如何构建一个兼顾可用性和一致性的分布式系统成为了无数工程师探讨的难题,出现了诸如CAP
和BASE
这样的分布式系统经典理论。
CAP定理
相信大多数人对CAP
理论都不会感到陌生,因为每个人在学习分布式的时候,或多或少都会接触到这个理论,而且也会知道这个理论中的C
、A
、P
分别代表的含义,接下来一起来回顾下。
CAP
理论告诉我们,一个分布式系统不可能同时满足一致性(C:Consistency
)、可用性(A:Availability
)和分区容错性(P:Partition tolerance
)这三个基本需求,最多只能同时满足其中的两个。
一致性
这里的一致性和ACID
中的一致性还是有点区别的,在分布式环境中,一致性是指数据在多个副本之间能否保持一致的特性。在一致性需求下,当一个系统在数据一致性状态下执行更新操作之后,应该保证系统的数据仍然处于一致的状态。这里强调的是副本之间数据的一致性,而非整个系统数据逻辑之间维护者一致性。
对于一个将数据副本分布在不同分布式结点上的系统来说,如果对第一个结点的数据进行了更新操作并且更新成功,却没有使得第二个结点上的数据得到相应的更新,于是在对第二个结点的数据进行读取操作时,获取的却依然是老数据(或称脏数据),这个就是典型的在分布式场景下的数据不一致情况。
如果分布式系统能够做到针对一个数据项的更新操作执行成功之后,所有的用户都可以读取到最新的值,那么这样的系统就可以称为具有强一致性(或严格一致性)。
可用性
可用性是指系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限时间内返回结果。这里需要注意的是“有限时间”和“返回结果”。
有限时间是指对于一个操作的请求,系统必须能够在指定时间内返回结果,如果超过了这个时间范围,那么就认为系统不可用。不同系统针对有限时间的定义范围是不同的,需要根据自身系统来判定最终的有限时间范围。
返回结果是指系统针对用户请求的操作,返回一个正常的响应结果,正常的响应结果通常能够明确地反映出对请求的处理结果,即成功或者失败,而不是一个用户不能理解的返回结果。例如系统因为运行抛出的异常,这些属于系统级错误,是不允许直接抛给用户的,所以一旦出现类似这种用户不理解的错误,我们都认为系统不可用。
分区容错性
分区容错性约束了一个分布式系统需要具有如下特性:分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
以上就是关于CAP
定理中关于一致性、可用性和分区容错性的讲解,而上文中我们也提到过,CAP
中最多同时只能满足其中两项,因此在进行对CAP
定理的应用我们就需要抛弃其中的一项,下面我们来分析一下,如果抛弃其中的任意一项,另外两项会出现什么问题。
-
放弃P
如果希望避免分区容错性,一种较为简单的做法是将所有的数据都放到一个分布式结点上,这样虽然不能百分百保证系统不会出错,但是至少不会出现因为网络原因导致分区出现负面影响,但是这样做整个系统的扩展性将大大降低。
-
放弃A
如果放弃容错性,那么就意味着,一旦系统遇到问题,那么受影响的服务就需要等待一段时间,而在等待期间内,系统无法对外提供正常的服务,即不可用。
-
放弃C
首先需要注意,这里所说的放弃一致性,并非是完全放弃一致性,因为完全放弃一致性,这样的系统运行起来也没有意义,而是放弃整个系统的强一致性,保留最终一致性,也就是说可以允许系统出现部分结点不一致的情况,但是系统承诺在指定时间范围内(晚间数据跑批进行数据核对),会保证系统数据最终是一致的。
从CAP
理论来看,一个分布式系统不可能同时满足一致性、可用性和分区容错性这三个需求。另一方面,需要明确的一点是,对于一个分布式系统而言,分区容错性可以说是一个最基本需求,如果这个都不满足,其实分布式存在的意义就没了,因此在系统架构时,往往需要把精力花在如歌根据业务特点在一致性和可用性之间寻求平衡。
BASE理论
BASE
是基本可用(Basicaly Acailable
)、软状态(Soft state
)和最终一致性(Eventually consistent
)三个短语的简写,是来自eBay
的架构师在CAP
理论的基础之上,通过对大规模互联网分布式系统实践中的总结而逐步演化来的,其核心思想是即使无法做到强一致性,但是每个应用都应该可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
基本可用
基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,需要注意的是,这里的损失部分可用并非是完全不可用。
例如,当系统正常时,一个请求的响应时间可能是100ms
内,而在系统出现问题时,可能这个请求的响应时间变成了1~2s
;同时还可能存在功能上的损失,比如在网站请求的高峰期,为例避免系统瘫痪,可以考虑将部分用户引导到一个降级页面。
弱状态
弱状态又称软状态,是指在系统运行过程中数据存在着一个中间状态,并认为该中间状态的存在不会影响系统的整体可用性。即允许系统在不同结点的数据副本之间进行同步的过程存在延时。
最终一致性
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
分布式一致性协议
在分布式系统中,每一个机器节点虽然能够明确地知道自己在进行事务操作过程中的结果是成功还是失败,但是却无法直接获取到分布式系统中的其他节点的操作结果,因此,当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的ACID
特性,就需要引入一个称为"协调者"(Coordinator
)的组件来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点则被称为"参与者"(Participant
)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务真正地进行提交。
在对一个分布式系统进行架构设计的过程中,往往会在系统的可用性和数据一致性之间进行反复的权衡,于是就产生了一系列一致性的协议和算法,下面来聊聊几种分布式一致性协议和算法。
2PC
2PC
,是Two-Phase Commit
的缩写,即二阶段提交,是计算机网络尤其是在数据库领域内,为了使基于分布式系统架构下的素有节点在进行事务处理的过程中能够保持原子性和一致性而设计的一种算法。
目前,绝大多数的关系型数据库都是采用二阶段提交协议来完成分布式事务的处理,利用该协议能够非常方便地完成所有分布式事务参与者的协调,统一决定事务的提交或回滚,从而能够有效保证分布式数据一致性,因此二阶段提交协议被广泛地应用在许多分布式系统中。
二阶段提交协议试讲事务的提交过程分成了两个阶段来进行处理,其执行流程如下:
阶段一:提交事务请求
1.事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2.执行事务
各参与者执行事务操作,并将Undo log
和Redo log
记录。
3.各参与者向协调者反馈事务询问的响应
如果参与者成功执行了事务操作,那么就反馈给协调者Yes
响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No
响应,表示事务不可以执行。
上述阶段主要是协调者组织个参与者的一次投票表态过程,所以这一阶段也叫“投票阶段”,即各参与者投票表明是否要继续执行接下去的事务提交操作。
阶段二:执行事务提交
在阶段二,协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包含以下两种可能:
执行事务提交
假如协调者从所有参与者那得到的反馈都是Yes
响应,那么就会执行事务提交。主要步骤如下:
-
发送提交请求
协调者向所有参与者发送
Commit
请求。 -
事务提交
参与者接收到
Commit
请求之后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源。 -
反馈事务提交结果
参与者在完成事务提交之后,向协调者发送
Ack
消息 -
完成事务
协调者接收到所有参与者的反馈
Ack
之后,完成事务。
中断事务
假如任何一个参与者向协调者反馈了No
响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
-
发送回滚请求
协调者向所有参与者节点发出
Rollback
请求。 -
事务回滚
参与者接收到
Rollback
请求之后,会利用其在阶段一种记录的Undo log
来执行事务回滚操作,并在完成回滚之后释放整个事务执行期间占用的资源。 -
反馈事务回滚结果
协调者在接收到所有的参与者反馈的
Ack
消息之后,完成事务中断。
这两个阶段的事务执行流程会有两个结果,分别如下:
事务提交
事务回滚
从图中我们可以看到,尽管二阶段提交已经比较完美了,但是还是会存在一些问题,我们来分析分析这个协议有什么优缺点:
优点:原理简单,实现方便。
缺点:同步阻塞,单点问题,脑裂,过于保守。
-
同步阻塞
二阶段提交协议在目前最明显的也是最大的一个问题就是同步阻塞,这回极大的限制分布式系统性能,在二阶段提交的过程中,所有参与该事务的操作逻辑都是出于阻塞状态的,也就是说,各个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作。
-
单点问题
在上面的讲解中,有一个十分致命的问题,那就是协调者是只有一个的,也就是说如果协调者出现问题,那么整个系统都将无法运转,更为严重的是,如果协调者在第二阶段出现问题,那么所有参与者都将一直处于锁定事务资源的状态中,而无法继续完成事务操作。
-
数据不一致
在二阶段提交第二阶段,当协调者向所有参与者发送
Commit
请求之后,如果发生了局部网络异常或者是协调者尚未发送完Commit
请求时发生了崩溃,那么便会导致只有部分参与者收到了Commit
请求,于是这部分收到了Commit
请求的参与者就会执行事务的提交,而其他没有收到Commit
请求的参与者将无法进行事务提交,于是整个分布式系统便出现了数据不一致的问题。 -
过于保守
如果在协调者向参与者发送事务提交询问的过程中,参与者因为自身故障导致协调者始终无法获取到所有的投票结果,这是协调者就只能依靠自身的超时判断机制判断是否需要中断事务,这样的策略过于保守,也就是说二阶段协议没有设计较为完善的容错机制,任意一个节点的失败,都会有可能导致整个系统失败。
3PC
在上一小节最后,我们提出了很多二阶段提交协议的问题,因此研究者在二阶段协议基础之上进行了改进,提出来了三阶段提交协议。
3PC
,是Three-Phase Commit
的缩写,即三阶段提交,是2PC
的改进版本,其在二阶段协议的基础之上,将二阶段协议的第一阶段一分为二,形成了由CanCommit
、PreCommit
和doCommit
三个阶段组成的事务处理协议,其协议涉及如图所示:
阶段一:CanCommit
-
事务询问
协调者向所有的参与者发送一个包含事务内容的
canCommit
请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。 -
各参与者向协调者反馈事务询问的响应
参与者在接收到来自协调者的
canCommit
请求之后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈Yes
响应,并进入预备状态,否则反馈No
响应。
这里需要注意,第一阶段参与者并没有执行任何操作,仅仅是查看自身是否具有执行接下来操作的条件,如果满足就返回Yes
,否则返回No
。
阶段二:PreCommit
在阶段二中,协调者会根据各自参与者的反馈情况来决定是否可以进行事务的PreCommit
操作,正常情况下包含两种可能:
执行事务预提交
假如协调者从所有的参与者获得的锁都是Yes
响应,那么就会执行事务预提交。
-
发送预提交请求
协调者向所有的参与者发送
preCommit
请求,并进入Prepared
阶段。 -
事务预提交
参与者接收到
preCommit
请求之后,会执行事务操作,并将Undo
和Redo
信息记录到事务日志中。 -
各参与者向协调者反馈事务执行的结果
如果参与者成功执行了事务操作,那么就会反馈
Ack
结果给协调者,同时等待最终指令:提交或回滚
中断事务
假如在第一阶段,存在一个参与者反馈的结果是No
,或者协调者在发送指令超时之后依然没有接收到参与者的反馈,那么就会中断事务。
-
发送中断请求
协调者向所有参与者发送事务中断指令(
abort
)。 -
中断事务
参与者无论是接收到了协调者发送的中断指令,还是反馈指令超时之后,依然没有接收到协调者发送的指令,参与者都会执行事务中断操作。
阶段三:doCommit
该阶段是事务的真正提交阶段,依然存在两种可能。
执行提交
-
发送提交请求
假设协调者处于正常工作状态,并且它收到了来自所有参与者的
Ack
响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送doCommit
请求。 -
事务提交
参与者接收到
doCommit
请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。 -
反馈事务提交结果
参与者在完成事务提交之后,向协调者发送
Ack
消息。 -
完成事务
协调者接收到所有参与者反馈的
Ack
消息后,完成事务。
中断事务
假设协调者处于正常工作状态,但是存在参与者向协调者发送了No
响应,或者在等待超时之后,协调者尚无法接收到参与者反馈的任何消息,那么就会执行中断事务。
-
发送中断请求
协调者向所有的参与者节点发送
abort
请求 -
事务回滚
参与者接收到
abort
请求之后,会利用其在阶段二中记录的Undo
信息来执行事务回滚操作,并在完成事务回滚之后释放在整个事务执行期间占用的资源。 -
反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送
Ack
消息。 -
中断事务
协调者在接收到所有参与者的反馈的
Ack
消息之后,中断事务。
到此第三阶段也结束了,但是需要注意的一点是,在阶段三可能会出现以下两种故障:
- 协调者出现故障
- 协调者和参与者之间的网络出现故障
这两中情况无论出现哪种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit
或是abort
请求,针对这样的异常情况,参与者都会在等待超时之后,继续进行事务的提交。
虽然三阶段提交协议已经比较完善,但是相信你看过以上内容之后,依然觉得三阶段提交协议好像还是会出现问题,那下面我们来看看三阶段提交协议的优缺点:
优点:相对于二阶段提交协议,三阶段提交协议降低了参与者的阻塞范围,并且能够出现在单点故障之后,后续继续达成一致性。
缺点:这个缺点我在上面也提到过,就是参与者在收到preCommit
消息之后,如果出现网络分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种条件下,参与者依然会进行事务的提交,这必然会出现数据不一致性。
总结
本文主要是作为一篇知识拓展文章,从集中式系统中数据库事务的ACID
四个特性入手,展开介绍了分布式条件下会遇到的一些问题,至此而引出了一些重要的分布式一致性协议,由于篇幅原因,本文仅仅介绍了两个比较简单的一致性协议,后续会再介绍当前分布式系统中常用的一致性算法–Paxos
算法,以及其变种–Raft
算法。
参考
- 倪超 《从Paxos到Zookeeper分布式一致性原理与实践》