分布式事务的最大挑战在于CAP,在《CAP理论与MongoDB一致性、可用性的一些思考》一文中有详细介绍。简而言之,由于网络分割(P: Network Partition)的存在,用户不得不在一致性(C Consistency)与可用性(A: Avaliable)之前做权衡。如果要保证强一致性(主要是应用层面的强一致性),那么在网络分割的时候,系统就不可用;如果要保证高可用性,那么就只能提供弱一致性,保证最终一致。下面提到的各种实现分布式事务的方法、协议都需要在一致性与可用性之间权衡。
2PC提到分布式事务,首先想到的肯定是两阶段提交(2pc, two-phase commit protocol),2pc是非常经典的强一致性、中心化的原子提交协议。中心化是指协议中有两类节点:一个中心化协调者节点(coordinator)和N个参与者节点(participant、cohort)。
顾名思义,两阶段提交协议的每一次事务提交分为两个阶段:
在第一阶段,协调者询问所有的参与者是否可以提交事务(请参与者投票),所有参与者向协调者投票。
在第二阶段,协调者根据所有参与者的投票结果做出是否事务可以全局提交的决定,并通知所有的参与者执行该决定。在一个两阶段提交流程中,参与者不能改变自己的投票结果。两阶段提交协议的可以全局提交的前提是所有的参与者都同意提交事务,只要有一个参与者投票选择放弃(abort)事务,则事务必须被放弃。
wiki上给出了简要流程:
注意,上图中洗下面一行也表明,两阶段提交协议也依赖与日志,只要存储介质不出问题,两阶段协议就能最终达到一致的状态(成功或者回滚)
而下图(来自slideshare)详细描述了整个流程:
在刘杰的《分布式原理介绍中》,有非常详细的流程介绍,可以配合上图一起看,另外还介绍了在各种异常情况下(比如Coordinator、Participant宕机,网络分割导致的超时)两阶段协议的工作情况。另外,在这篇文章中也有比较清晰的流程介绍。在这里只讨论2PC的优缺点:
优点:强一致性,只要节点或者网络最终恢复正常,协议就能保证顺利结束;部分关系型数据库(Oracle)、框架直接支持
缺点:两阶段提交协议的容错能力较差,比如在节点宕机或者超时的情况下,无法确定流程的状态,只能不断重试;两阶段提交协议的性能较差, 消息交互多,且受最慢节点影响
这篇文章描述了为什么两阶段提交协议在分布式系统中不适用:
系统“水平”伸缩的死敌。基于两阶段提交的分布式事务在提交事务时需要在多个节点之间进行协调,最大限度地推后了提交事务的时间点,客观上延长了事务的执行时间,这会导致事务在访问共享资源时发生冲突和死锁的概率增高,随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平伸缩的"枷锁", 这是很多Sharding系统不采用分布式事务的主要原因。
所言甚是!
3PC三阶段提交协议(3pc Three-phase_commit_protocol)主要是为了解决两阶段提交协议的阻塞问题,从原来的两个阶段扩展为三个阶段,并且增加了超时机制。
3PC只是解决了在异常情况下2PC的阻塞问题,但导致一次提交要传递6条消息,延时很大。具体流程描述可参见《关于分布式事务、两阶段提交协议、三阶提交协议 》一文。
TCCTCC是Try、Commit、Cancel的缩写,在国内由于支付宝的布道而广为人知,TCC在保证强一致性的同时,最大限度提高系统的可伸缩性与可用性。
我们假设一个完整的为业务包含一组子业务,Try操作完成所有的子业务检查,预留必要的业务资源,实现与其他事务的隔离;Confirm使用Try阶段预留的业务资源真正执行业务,而且Confirm操作满足幂等性,以遍支持重试;Cancel操作释放Try阶段预留的业务资源,同样也满足幂等性。“一次完整的交易由一系列微交易的Try 操作组成,如果所有的Try 操作都成功,最终由微交易框架来统一Confirm,否则统一Cancel,从而实现了类似经典两阶段提交协议(2PC)的强一致性。”
与2PC协议比较 ,TCC拥有以下特点:
位于业务服务层而非资源层 ,由业务层保证原子性
没有单独的准备(Prepare)阶段,降低了提交协议的成本
Try操作 兼备资源操作与准备能力
Try操作可以灵活选择业务资源的锁定粒度,而不是锁住整个资源,提高了并发度
当然,TCC需要较的高开发成本,每个子业务都需要有响应的comfirm、Cancel操作,即实现相应的补偿逻辑。
基于消息的分布式事务这类事务机制将分布式事务分成多个本地事务,这里称之为主事务与从事务。首先主事务本地先行提交,然后通过消息通知从事务,从事务从消息中获取信息进行本地提交。可以看出这是一种异步事务机制、只能保证最终一致性;但可用性非常高,不会因为故障而发生阻塞。另外,主事务已经先行提交,如果因为从事务无法提交,要回滚主事务还是比较麻烦,所以这种模式只适用于理论上大概率等成功的业务情况,即从事务的提交失败可能是由于故障,而不大可能是逻辑错误。
基于异步消息的事务机制主要有两种方式:本地消息表与事务消息。二者的区别在于:怎么保证主事务的提交与消息发送这两个操作的原子性。
如果用异步消息实现转账的例子,那么操作分为四部:用户A扣钱,发消息,用户B收消息,用户B扣钱。前两步必须保证原子性,如果A扣钱成功但是没有发出消息,那么用户A损失了;如果发消息成功,但是没有扣钱,那么用户B就多得了一笔钱,银行肯定不干。
本地消息表基于本地消息表的方案是指将消息写入本地数据库,通过本地事务保证主事务与消息写入的原子性。例如银行转账的例子,伪码如下:
begin transaction: update User set account = account - 100 where userId = 'A'
insert into message(userId, amount, status) values('A', 100, 1)
commit transaction
然后通过pull或者push模式,从业务获取消息并执行。如果是push模式,那么一般使用具有持久化功能的消息队列,从事务务订阅消息。如果是pull模式,那么从事务定时去拉取消息,然后执行。
mongodb的写入就很像本地消息表,在WriteConcern为w:1的情况下,更新操作只要写到oplog以及primary就可以向客户端返回。secondary异步拉取oplog并本地记录执行。
事务消息: