分布式事务基础整理


事务的存在保证了一组操作按照事务的规则正确执行,并且多组操作之间不会相互影响。传统的数据库事务有 ACID 四个特性。在单数据库模式下,ACID 模型能有效保障数据的完整性,但是在大规模分布式环境下,一个业务往往会跨越多个数据库。例如,为了保证数据的高可用,通常我们会将数据保留多个副本 (replica),这些副本会放置在不同的物理的机器上。如何保证这多个数据库之间的数据一致性,需要其他行之有效的策略。

所以在理论计算机科学领域,Eric Brewer提出了著名的CAP定理(CAP teorem, also named Brewer's theorem),即分布式数据存储不可能同时提供 Consistency、Availability、Partition tolerance 中的两种以上。

CAP定理

真正的CAP定理其实是:在存在网络分区的情况下,必须在一致性和可用性之间进行选择。 CAP中的CAP具体如下:

一致性 (Consistency):保证每次读 (read) 只能读到最近的写 (write),否则报错
可用性 (Availability):保证每次读都不会返回错误,甚至读到的数据并不是最近写入的
分区容错性 (Partition tolerance):保证存在网络分区时系统能正常运行。如果网络故障产生网络分区时,分区两边的处理可以继续,那就是分区容忍的。

可以看到,A和C之间是相互冲突的。换句话说,CAP就是要P的前提下选择A或者C。

分区容错性必须吗

首先来看分区容错性的定义,Seth Gilbert 和 Lynch 的论文是这样定义分区容忍性的:网络允许丢失以一个节点发给另一个节点的任意多的消息。

分布式通常假设网络是异步的,意味着网络可能会导致任意的重复、丢失、延迟或者乱序的节点间消息传递。在实际中,TCP状态机会保证节点间消息传递的可靠性。但在Socket级别上,节点接发消息会阻塞,超时等等。原本两个在同一网络下,能相互通信的集群,因为一些问题,如网络故障、机器故障,无法继续相互通信,就会产生网络分区 (network partition) 。而当一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。

例如集群A和B目前在同一内网下正常通信,它们就是一个分区。

如果因为某个原因网关炸了,就会成为这个样子:

所谓的分区容忍性,就是说一个数据服务的多台服务器在发生了上述情况的时候,依然能继续提供服务。提高分区容忍性,可以将一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。

然而,由于网络通信的不确定性,多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功。等待时间可能很长,这就违背了可用性的初衷:为了效率可以返回过期数据。

所以总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。

Eric Brewer 的观点认为,网络分区是客观存在的事实,是一定有可能会发生的情况,所以P是必须要有的,A和C只能而选一。

但是A和C并不是0和1的关系,而是一个权衡,例如TCC在保证强一致性的同时最大程度去提高可用性,本地消息表在高可用的同时也会保证最终一致性。所以CAP理论带来的价值是指引我们在设计分布式系统时需要区分各种数据的特点,并仔细考虑在小概率的网络分区发生时究竟为该数据选择可用性还是一致性。

当然现在也有人认为分区其实是可以避免的,例如分区发生时,通过把请求导入到最大的分区,可以移除分区,实现CAP。大部分NoSQL实现在分区(节点故障)时仍然是高可用和一致的,因为节点故障是失败-恢复模型,不是失败-停止模型。前者在发生在节点应用故障或机器负载高,暂时不可用时,而后者发生在机器宕机,长时间稳定不可用时,显然前者更常见,不然就不需要Paxos,3PC就够了。

例如Redis集群的故障恢复机制,在主数据库下线时通过Raft算法选举一个从库作为新的主库,而不是直接停止服务。只有当没有从库可以恢复且设置cluster-require-full-coverage为no才会停止服务。

ACID模型和BASE模型

我们都很熟悉数据库的 ACID 四个特性,即所谓的原子性、一致性、隔离性和持久性。传统的ACID原则中,一致性和隔离性的要求非常严格,所有事务看到的数据必须是最近一次写入的,两个事务之间对数据的操作必须互不影响。所以ACID只能在比较理想化的场景实现,即使在单机数据库时代,也基本不会完全按照 ACID 特性,MySQL 为了提高事务并发能力,对隔离性会适当放松(四种隔离级别)而导致一致性也会有强弱。

在分布式系统中也不例外。为了可用性、性能与降级服务的需要,必须降低一致性 ( C ) 与 隔离性( I ) 的要求。 所以基于CAP,就有了所谓的BASE模型。

BASE的核心是最终一致性(Eventually consistent),即当某个数据被写事务多次修改时,只保证其他读事务能读取到最后一次修改,之前的修改并不保证能被读事务感知。

因为强一致性被放宽为最终一致性,所以不用再担心由于网络消息的异步性导致的分布式系统节点之间数据不一致现象,只需要保证整个分布式事务的一致性即可。所以分布式事务的实现开始变得多样化起来,例如3PC、TCC或者消息表,都是最终一致性的。

XA规范和2PC协议

其实两台机器理论上永远无法达到一致的状态,因为相互独立的节点之间无法准确的知道其他节点中的事务执行情况。一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果。所以他也就不知道本次事务到底应该commit还是 roolback。如果想让分布式部署的多台机器中的数据保持一致性,那么就要引入一个“协调者”的组件来统一调度所有分布式节点的执行。

1994年,X/Open 组织(即现在的 Open Group )定义了分布式事务处理X/Open DTP模型。这个模型引入了事务管理器(TM)作为协调者角色,另外,它还定义了其它的角色。

 

XA 就是 X/Open DTP 定义的事务管理器(如交易中间件)与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。

两段提交协议(2PC)就是就是根据这一思想衍生出来的,可以说二阶段提交其实就是实现XA分布式事务的关键。

2PC主要保证了分布式事务的原子性。
二阶段提交的思路非常简单明朗,实现也很容易。其思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。具体如图:

可以看到,两段提交协议本质上还是强一致性的,事实上,XA协议遵循强一致性。在单个数据库收到prepare请求后会开始自己的本地事务并进行预处理,包括资源上锁、事务执行、写undo/redo log等,然后等待commit或者rollback请求到来。这里有一个很坑的地方,就是在commit或者rollback请求到来之前,会一直锁住操作的数据,这样就并发性就非常低。而且如果事务管理器炸了,就会出现所谓的单点故障问题导致数据被锁死。

了解更多可以查看关于分布式事务、两阶段提交协议、三阶提交协议

蚂蚁金服的XTS继承了2PC简单易理解的思想,利用BASE模型进行优化。相比传统二阶段模式,减少持有锁时间,大幅提升性能。

 

参考文献

  1. Wikipedia: CAP theorem
  2. CAP Confusion: Problems with 'partition tolerance'
  3. Redis Cluster故障恢复机制

 

 

声明:夜语|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 分布式事务基础整理


我不和你谈事,只想和你谈心。🍂