分布式事务原理

Posted by 皮皮潘 on 12-09,2021

XA强一致性分布式事务

XA中主要定义了三个核心组件,分别是应用程序、资源管理器和事务管理器

应用程序: 定义各个分支事务边界,即开始和结束同时在事务边界内对资源进行操作

资源管理器:主要由数据库本身担任,提供访问、操作资源的能力

事务管理器:负责分配事务唯一标识,监控事务的执行进度,并负责全局事务的Prepare以及Commit/Rollback

XA规范如下:1. xa_start:负责开启一个事务分支 2. xa_end:负责结束一个事务分支(但是没有提交) 3. xa_prepare:两阶段提交中的准备阶段 4. xa_commit:负责提交事务分支 5. xa_rollback:负责回滚事务分支

XA执行流程如下:[xa start -> execute * n -> xa end] * n个资源管理器 -> [xa prepare * n -> xa commit/rollback * n](两阶段提交)

XA的主要问题:1. 同步阻塞:全局事务内部包含多个事务分支,这些事务分支只要等到了全局事务可以提交(所有参与者都执行完了事务分支且Prepare返回成功)才会正式提交,XA的核心就是使用各个事务分支的ACID特性共同构成全局事务的ACID特性,但是这也导致了极大的性能损耗 2. 单点故障:如果事务管理器炸了,那么整个XA事务以及锁住的资源都会阻塞下去

AT强一致性分布式事务

AT也是一个两阶段提交的解决方案,但是相较于XA,AT可以做到无侵入的分布式事务,在AT模式下用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,框架会自动生成事务的二阶段提交和回滚操作,其具体原理如下:

在一阶段,框架拦截”业务SQL“,通过解析对应的SQL语义,找到要更新的业务数据,并将被更新前的业务数据保存为”Before Image“,然后执行”业务SQL“更新业务数据,然后将更新后的业务数据保存为”After Image“(用于比较避免脏写),最后生成行锁,避免被其他事务更新,以上操作全部在一个数据库事务内完成,保证了一阶段操作的原子性,需要注意此时和XA一样,事务并没有提交

在二阶段,如果是提交的话,因为“业务 SQL”在一阶段已经执行, 所以框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理并提交事务即可;如果是回滚的话,框架就需要回滚一阶段已经执行的“业务 SQL”还原业务数据,回滚方式便是用“Before Image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “After Image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

TCC最终一致性分布式事务

TCC的核心在于用户在业务层实现Try,Confirm以及Cancel三个接口,同时引入中间状态(Soft State)

Try接口:1. 完成业务检查,确保数据的一致性 2. 预留必要的业务资源,确保数据的隔离性。这里需要注意的一点是,Try接口也会修改数据库,但是不是直接将业务数据修改为最终状态,而是修改为中间状态比预留资源,比如:将订单状态修改为”支付中“的中间状态、库存数量减去对应用户的下单数同时在待扣减库存数量中增加对应的数量

Confirm接口:由于Try接口已经完成了业务检查,因此Confirm接口中直接进行业务操作,将数据库的业务数据中间状态直接修改为最终状态

Cancel接口:如果在调用某个服务的Try接口失败了,或者调用某个服务的Confirm接口失败,此时就要调用所有服务的Cancel接口,去回滚Try接口或者Confirm接口中执行的操作。这里需要注意的一点是,Cancel接口既可能回滚Try也可能回滚Confirm

一个完整的TCC分布式事务主要包含三个服务:1. 主业务服务:发起TCC分布式事务的发起方(总需要有某个业务去发起整个分布式业务,另外主业务服务也可能参与分布式事务,此时主业务服务同时也是从业务服务) 2. 从业务服务:在整个TCC分布式事务中被调用的业务服务 3. TCC管理器:在整个TCC分布式事务的执行过程中,管理并控制着整个事务活动,包括记录并维护全局事务以及分支事务状态,自动判断并调用对应的Confirm或者Cancel接口

TCC的核心在于将原本一个接口完成的事拆分成三个接口TCC,同时执行流程如下:1. 依次执行所有参与者的Try接口 2. 如果每个Try接口都执行成功,则依次执行所有参与者的Confirm接口 3. 如果有Try接口执行失败或者Confirm接口执行失败,则依次执行所有参与者的Cancel接口

TCC的另外一个核心在于通过Try接口结合中间状态实现资源的预分配:举例如下,在电商支付订单的业务场景中,对于订单服务,不能将订单状态直接更新为支付成功,而是通过Try接口更新为支付中的中间状态,对于库存服务,也不能直接简单地扣减库存,而是通过Try接口在扣减库存的同时在冻结库存的字段中保存扣减库存的数量,最后再通过Confirm接口将订单状态更新为已成功,将库存服务中的冻结库存减去扣减库存数量,也即修改为最终状态

相较于XA以及AT,TCC的Try、Confirm以及Cacnel接口各自是一个独立的事务而不是同一个事务,因此不需要等待全局的Commit,从而解决了同步阻塞的问题,大大地提升了性能,但是该方案的侵入性也很强,需要用户自行实现对应的TCC三个接口

可靠消息最终一致性分布式事务

可靠消息的核心在于:1. 发送消息的可靠性 2. 接收消息的可靠性;其中可靠性的核心在:1. 中间状态 2. 重试 3. 幂等 4. 可查询事务

发送消息的一致性

发送消息的可靠性主要有两种实现方案:

本地消息表

为了防止出现消息丢失的情况,使用本地事务保证数据业务操作和消息的原子性,也就是通过本地事务,将业务数据和消息数据分别保存到本地数据库的业务数据表和本地消息表中,然后通过定时任务读取本地消息表中的消息数据,将消息发送到消息中间件,等到消息消费者成功接收到消息后,再将本地消息表中的消息删除。这种方案实现简单,但是耦合性太重,每个需要实现分布式事务的系统都要独立实现一套对应的本地消息表,因此又出现了另外一套实现方案。

独立消息服务

独立消息服务在本地消息表的基础上,将消息服务独立出来,将消息数据从本地消息表独立成单独消息数据库,同时引入消息确认服务和消息恢复服务,另外也需要事务发起方实现事务查询接口

其具体流程如下:1. 事务发起方向消息服务发送一个初始消息,状态为待确认(中间状态) 2. 消息服务接收到消息后,将消息存储到本地数据库,并返回消息存储结果 3. 事务发起方在收到消息存储成功后,执行本地业务事务并在完成后把业务处理的结果发送给消息服务 4. 消息服务接收到业务结果,如果为成功则更新消息状态为待发送并且使用消息中间件投递消息,反之则删除对应的消息 5. 为了保证事务发起方一定能把消息发送出去(防止由于网络波动,导致消息服务没有接收到对应的事务结果),在消息服务中会通过消息确认服务定时扫描事务发起方对应的事务查询接口,如果发现事务已经执行结束但是消息仍然是待确认的状态则主动根据事务结果更新对应的状态

接收消息的一致性

发送消息的一致性通过消息服务进行保证,事务发起方进行辅助,而接收消息的一致性则需要由事务参与方保证,消息服务进行辅助:1. 消息服务通过消息中间件向事务参与方投递消息如果失败,则会通过消息恢复服务定时扫描发送失败并且状态为”待发送“的消息,将其投递给事务参与方 2. 如果消息正确投递出去,则将对应状态更新为”已发送“ 3. 事务参与方接收到消息后执行业务逻辑并将执行的结果发送给消息服务 4. 消息服务接收到执行结果后,根据执行结果,如果执行成功则删除对应的消息,如果执行失败则根据业务逻辑是重试还是事务发起方执行对应的补偿操作 5.由于事务参与方将执行的结果发送给消息服务也有可能投递失败,此时同样需要引入重试机制,定时扫描本地数据库中状态为执行完成但是发送消息失败的记录并重新投递对应的消息

除此之外为了保证消息发送与接收的可靠性,需要引入最大重试次数防止重试过多而崩溃,同时需要保证业务执行的幂等性,因为消息会被反复投递