事务是一段访问或者更新数据的程序。它的特点是ACID。本地事务只涉及到一个数据库能够保证事务ACID了。当涉及到多个不同数据库(广义的数据库,他们可以是MQ,甚至缓存)操作时,为了保证每个数据库的操作要么都成功或者都失败,就需要额外的技术来处理。这是因为单个数据库操作会失败,同时通信失败会导致整个事务无法感知这个失败。二阶段提交(2PC)或者三阶段提交(3PC)就是用来解决这个问题。
一般来说这个有两个角色一个是事务协调者,简称TC,一个是事务参,简称TP。下文用简称来说明。
当TP把事务中修改的结果持久化到存储以后,它才能算是准备完毕。
在所有的TP准备完毕之前,不能有任何一个TP提交事务。不然就会破坏事务的原子性。比如:TP1提交了,TP2还没准备完毕,这个时候TP2crash了,因为TP2并没有保存事务相关的after-images,就没法恢复。这时就出现了TP1成功,但是TP2没有执行的情况。
2PC本质:在提交之前确保所有的TP都已经准备完毕。
2pc包含两个阶段。准备阶段和提交(终止)阶段,TC和TP的通信如下。
TC TP
+----+ REQUEST-TO-PREPARE +----+
| | -----------------------> | |
| | PREPARE | |
| | <----------------------- | |
| | NO | |
| | | |
| | COMMIT | |
| | -----------------------> | |
| | ABORT | |
| | DONE | |
| | <----------------------- | |
+----+ +----+
Messages
2pc本质是任何TP提交之前,所有的TP都必须已经准备好。
第一阶段
第二阶段
当TP发送完PREPARE之后,收到COMMIT或者ABORT之前,它无法做任何操作。因为,它不知道该提交还是终止事务。这个时候,TP就会处在一直等待状态。因此,当TC挂了,同时也没有其他已经提交的TP情况下,事务就会一直阻塞在那里。
等待消息超时时的处理。
TC视角
向TP发送REQUEST-TO-PREPARE消息
没等待消息,没有处理。
等待TP的PREPARE或者NO消息
一段时间以后没有收到所有TP的PREPARE消息或者某个TP的NO消息,TC自己决定终止事务。向每个TP发送ABORT消息。
决定提交还是终止
没等待消息,没有处理。
向TP发送COMMIT或者ABORT消息
没等待消息,没有处理。
等待TP的DONE消息
等待超时以后,重新发送COMMIT或者ABORT消息。这个消息除了事务本身资源没有释放,其他资源都可以释放。
释放事务资源
没等待消息,没有处理。
TP视角
等待TC发送REQUEST-TO-PREPARE消息
一段时间以后没有收到所有TC的消息,TP自己决定终止事务。如果,后面再收到REQUEST-TO-PREPARE回复NO消息。
进行PREPARE操作
没等待消息,没有处理。
如果(2)成功向TC发送PREPARE,否则发送NO
没等待消息,没有处理。
等待TC的COMMIT或者ABORT消息
阻塞,一直等待,超时执行终止协议。
向TC发送DONE消息
没等待消息,没有处理。
当TC或者TP宕机重启以后需要日志辅助事务的恢复。如果没有日志,事务就没法正常结束了。
日志
TC TP
+-------------------+ +-------------------+
1 ===> | | |
|log start-two-phase| | |
| | REQUEST-TO-PREPARE | |
| | -----------------------> | <=== 1
2 ===> | | |
| | PREPARE | log prepare |
| | <----------------------- | |
| | NO | |
| log commit/abort | | <=== 2
| | | |
3 ===> | COMMIT | |
| | -----------------------> | |
| | ABORT | |
| | | |
| | DONE | log commited |
| | <----------------------- | |
| log done | | <=== 3
| | | |
4 ===> | | |
+-------------------+ +-------------------+
如上图TC需要3条日志,TP需要2条日志。
TC日志
log start-two-phase
在开始2PC之前记录start-two-phase日志。包含所有TP的信息,不然TC恢复的时候,就不知道哪些TP,也就无法发送恢复信息了。
log commit/abort
在提交之前记录commit/abort日志。不然,TC发送了COMMIT消息之后失败,恢复的时候就不知道到底是提交还是失败。
log done
当收到所有的TP的DONE消息后,记录done日志。恢复的时候可以用来清除事务资源。
TP日志
log prepare
在发送PREPARE消息之前记录prepare日志。当TP没有记录日志的情况下发送了PREPARE消息给TC然后crash,恢复的时候,因为没有准备和提交日志,就不知道自己的状态了。这个时候,就会终止事务。特别在它已经发送了PREPARE消息情况下,而TC已经提交事务,就会造成数据的不一致。
log commited
在收到TC的COMMIT或者ABORT的消息以后在发送DONE之前记录commited日志。这样恢复的时候知道自己的状态,不然就会陷入不确定状态。同时,可以尽快释放一些锁资源。
恢复处理
通过上面的日志,TC有四种恢复情况,如上图:
通过上面的日志,TP有三种恢复情况,如上图:
没有任何日志。终止事务。
有prepare日志,还没有commited日志。执行终止协议。
有commited日志。向TC发送DONE消息,或者等TC发送COMMIT或者ABORT的时候回复DONE消息。
终止协议是说当TP恢复的时候终止事务的协议。最简单的做法就是等到恢复和TC的通信,从TC哪里得到消息。这样的坏处是一直阻塞到和TC的通信恢复为止,只要TC挂了事务永远阻塞。
2PC是XA规范的标准实现。最重要的问题是阻塞,即当TC以及知道事务状态的TP都挂的情况下,事务没法终止。
2PC之所以会阻塞是因为通信失败的原因会发生当一个TP处在不确定状态时,不确定其他TP是否已经终止或者提交,从而使TP一直阻塞。如果能够确保任何TP提交时确保不会存在不确定状态的TP,就不会阻塞事务了。3PC是2PC的一种改进,主要是把第二阶段拆分为两个阶段,通过PRE-COMMIT消息做到所有的TP都直到其他TP已经确定可以提交。
2PC在只出现机器故障的时候,它也是一定阻塞的。而3PC可以避免这个问题,如果出现通信故障,还是没法避免阻塞,可以降低阻塞的频率。实际系统中,由于阻塞的概率比较低、3PC实现负责以及性能比较低,因此使用的都是2PC。
TC TP
+----+ VOTE-REQ +----+
| | -----------------------> | |
| | YES | |
| | <----------------------- | |
| | NO | |
| | | |
| | PRE-PREPARE | |
| | -----------------------> | |
| | ABORT | |
| | ACK | |
| | <----------------------- | |
| | DONE | |
| | | |
| | DO-COMMIT | |
| | -----------------------> | |
| | DONE | |
| | <----------------------- | |
+----+ +----+
Messages
第一阶段
第二阶段
第三阶段
等待消息超时时的处理。
TC视角
向所有的TP发送VOTE-REQ消息
没等待消息,没有处理。
等待TP的YES或者NO消息
一段时间以后TC没有收集到所有TP的YES消息或者某个TP的NO消息,自行终止事务,并向每个返回YES消息的TP发送ABORT消息。
决定预提交或者终止
没等待消息,没有处理。
等待TP的ACK消息
一段时间以后TC没有收到所有TP的ACK消息。向TP发送DO-COMMIT消息,就当所有的TP都发回了ACK消息。因为,毕竟所有的TP针对VOTE-REQ消息的回答是YES,说明是可以提交的。
等待TP的DONE消息
等待超时以后,重新发送DO-COMMIT消息。这个消息除了事务本身资源没有释放,其他资源都可以释放。
TP视角
等待TC的VOTE-REQ消息
一段时间以后TP没有收到VOTE-REQ消息,自行终止事务。
发送YES或者NO消息
没等待消息,没有处理。
等待TC的PRE-PREPARE或者ABORT消息
阻塞,一直等待,超时执行终止协议。
发送ACK或者DONE消息
没等待消息,没有处理。
等待TC的DO-COMMIT消息
阻塞,一致等待,超时执行终止协议。这是因为,这个点上TP能够直到其他的TP都已经同意执行事务了。但是,有可能一个TP因为TC没有向它发送PRE-COMMIT就挂了,这条TP就会处于不确定状态。这样就会出现一个TP已经提交了,一个TP还不确定。如果,已经确定状态的TP都挂了,事务就没法终止。
发送DONE消息
没等待消息,没有处理。
日志
日志内容和2PC是一样的。
TC TP
+---------------------+ +-------------------+
1 ===> | | |
|log start-three-phase| | |
| | VOTE-REQ | |
| | -----------------------> | <=== 1
| | YES | log prepare |
2 ===> | <----------------------- | |
| | NO | |
| | | <=== 2
| | | |
| | PRE-PREPARE | |
| log commit/abort | -----------------------> | |
| | ABORT | |
3 ===> | ACK | log commited |
| | <----------------------- | |
| | DONE | <=== 3
| | | |
| | DO-COMMIT | |
| | -----------------------> | |
| log done | DONE | |
| | <----------------------- | |
4 ===> | | |
+---------------------+ +-------------------+
TC日志
log start-three-phase
在开始3PC之前记录start-three-phase日志。包含所有TP的信息,不然TC恢复的时候,就不知道哪些TP,也就无法发送恢复信息了。
log commit/abort
在提交之前记录commit/abort日志。不然,TC发送了PRE-PREPARE消息之后失败,恢复的时候就不知道到底是提交还是失败。
log done
当收到所有的TP的DONE消息后,记录done日志。恢复的时候可以用来清除事务资源。
TP日志
log prepare
在发送YES消息之前记录prepare日志。当TP没有记录日志的情况下发送了YES消息给TC然后crash,恢复的时候,因为没有准备和提交日志,就不知道自己的状态了。这个时候,就会终止事务。特别在它已经发送了YES消息情况下,而TC已经提交事务,就会造成数据的不一致。
log commited
在收到TC的PRE-PREPARE或者ABORT的消息以后在发送ACK或者DONE之前记录commited日志。这样恢复的时候知道自己的状态,不然就会陷入不确定状态。
恢复处理
通过上面的日志,TC有四种恢复情况,如上图:
通过上面的日志,TP有三种恢复情况,如上图:
不管是2PC还是3PC当TP恢复的时候,如果处在不确定状态就需要执行终止协议。在确定了事务状态的情况,最简单的做法等待恢复和TC之间的通信,获得事务状态。还有,通过其他TP的获得事务状态。如果事务状态还不确定,可以通过重新开始事务或者终止事务的方式搞定。详细的算法《Concurrency Control and Recovery in Database Systems》中7.5节有详细说明。