当前位置: 首页 > 科技观察

浅谈分布式数据库对2PC的优化

时间:2023-03-23 10:52:51 科技观察

本文转载自微信公众号“jinjunzhu”,作者jinjunzhu。转载本文请联系jinjunzhu公众号。在单体数据库时代,数据库本身就支持ACID事务。开发者甚至需要在方法上加上@Transactional注解即可完成交易,非常简单。但是在分库分表分布式数据库时代,传统数据库的ACID特性只能在单节点上工作,全局事务需要一个全局事务管理器来维护,非常复杂。在分布式事务领域,使用最广泛的全局事务引导方案是2PC,也称为两阶段提交,但是2PC也有一些缺陷。今天我们就来看看分布式数据库是如何优化这些缺陷的。两阶段提交(2PC)有两种主要类型的两阶段提交协议。一种是应用层的TCC。比如阿里巴巴的seata就实现了TCC模式。这种模式的特点是每个服务都需要提供try/confirm/cancel这三个实现,这三个实现都需要在业务代码中实现,对业务的侵入性很强。今天分享的是面向资源的2PC协议,它最早是由JimGray提出的。整个事务分为两个阶段,准备阶段和提交阶段。这两个阶段由协调节点和DB资源管理器完成。这里我们仍然以经典的电子商务系统为例。整个系统分为三个服务:订单、账户和库存。我们收到客户的购买请求后,协调节点需要协调订单服务生成订单,账户服务扣除商品款。库存业务扣除商品库存。如果这三个服务的数据库在不同的分片上,协调过程如下:1.在准备阶段,协调节点向所有服务发送准备请求。每个服务收到准备请求后,都会尝试执行本地事务,但不会真正提交本地事务。这个尝试执行的过程会检查是否满足执行事务的条件,比如资源是否被锁定等,当所有服务尝试执行成功后,协调节点会返回yes,如图下图:2.commit/rollback阶段如果prepare阶段所有服务都返回yes,那么协调节点会通知各个服务执行commit操作,各个服务才会真正提交本地事务。如下图所示:如果一个服务在prepare阶段返回no,协调节点需要通知所有服务回滚本地事务。2PC存在的问题上面我们简单分析了2PC协议的执行过程,那么2PC存在哪些问题呢?1.性能问题本地事务在准备阶段锁定资源。先锁定小明账号。这样,如果有其他交易修改小明账户,则必须等待之前的交易完成。这会造成延迟和性能下降。2.协调节点单点故障协调节点是单节点。如果发生故障,整个事务将始终被阻塞。例如,在第一阶段,prepare成功,但在第二阶段,协调节点在发出commit命令之前就宕机了。服务的所有数据资源都被锁定,后续事务只能等待。3.数据不一致如果prepare第一阶段成功,但是第二阶段commit,如果协调节点通知库存服务失败,这相当于生成订单扣账,但不扣库存。这会导致数据不一致。Percolator模型主流的NewSQL数据库,比如TiDB,都是通过Percolator模型来解决的。官网链接如下:https://pingcap.com/blog-cn/percolator-and-txn/Percolator模型来自Google论文:《Large-scaleIncrementalProcessingUsingDistributedTransactionsandNotifications》原文见下方链接,还有网上有很多翻译版本:https://www.cs.princeton.edu/courses/archive/fall10/cos597B/papers/percolator-osdi10.pdfPercolator的前提是本地事务数据库支持多版本并发控制协议,也就是mvcc。现在支持mysql、oracle等主流数据库。a)初始阶段,我们看上面我们提到的经典电商案例。初始阶段,我们假设订单数量为0,账户服务为1000,库存服务为100。客户下单后,订单服务增加1个订单,账户服务扣除金额100,库存服务扣除1的产品数量。每个切片的初始数据如下:前面的“:”是时间戳或数据版本,后面是数据值。这三张表中,第一条记录保存的不是真实数据,而是指向真实数据的指针。例如订单表中,版本6的数据指向版本5的数据,订单数量为0。b)准备阶段在准备阶段,协调节点向各个服务发送准备命令,三张表分别进入prepare阶段。在准备阶段,Percolator定义了主锁的概念。每个分布式事务中只有一个服务可以获得主锁,比如本例中的订单服务,其他服务的锁都指向主锁的指针,如下表所示:准备阶段,每个服务都会写日志,根据时间戳记录交易的私有版本,这样其他交易就无法操作这三块数据。c)提交阶段在提交阶段,协调节点只需要和订单服务通信,因为订单服务有主锁,也就是说协调节点只和有主锁的分片通信。此时数据如下:这时候我们注意到除了订单服务的锁没有了,增加了版本8指向版本7,说明没有订单服务的私有版本,但帐户服务和库存服务的私有版本仍然存在。Percolator的独特之处就在这里,它会启动一个异步线程来更新账户服务和库存服务。最终数据如下:因为协调节点只需要和获取主锁的分片通信,所以要么成功,要么失败。这样就避免了提交时所有节点都不能成功导致的数据不一致问题。在准备阶段,记录日志。如果某个分片提交失败,可以根据日志重新提交,从而保证数据的最终一致性。如果协调节点宕机,异步线程可以释放资源,避免单点故障通信失败导致资源无法释放。这里要注意两点:主锁的选择是随机的。例如本例中,订单服务协调节点发送commit后,并不一定选择订单服务提交成功。这时候如果其他事务需要读取accountservice和inventoryservice2条数据,虽然这2条数据还是有锁的,但是查找primary@order.bal后发现已经被已提交,以便阅读。总结一下,2PC协议存在三个问题,性能问题、单点故障和数据不一致。Percolator模型简化了协调节点与分片之间的通信过程,让协调节点只与其中一个主分片通信。一方面,它减少了通信开销。另一方面避免了由于单点故障导致commit阶段部分节点通信失败。数据不一致问题。Percolator在prepare阶段记录日志,这样即使协调节点出现故障,恢复后也可以根据日志进行事务恢复。Percolator采用异步线程的方式来释放资源,即使协调节点出现故障,也不用担心资源释放不出去。著名的NewSQL数据库TiDB参考Percolator模型对2PC协议进行了优化。但是我们要知道2PC的性能问题依然存在。好在主流的分布式数据库都做了优化,性能损失只会越来越小。