分布式事务
分布式事务一般是在微服务架构中,涉及多个服务对应的多个数据库或系统的数据操作,这些操作需要被保证一致性。
2PC
两阶段提交(Two-Phase Commit,2PC)是一种分布式事务协议,是为了实现刚性事务的协议。它将事务的提交过程分为两个阶段,第一阶段为准备阶段,第二阶段为提交阶段。
这个的具体机制比较像 InnoDB 中 RedoLog 和 BinLog 保证事务性的机制。用于 Crash Recovery 的 RedoLog 先写入并标记为 prepare 状态,之后作为整个数据库日志的 BinLog 写入,同时会标记 RedoLog 为 commit 状态。
分布式事务中的 RedoLog 对应的就是 Participant(参与者)节点,而 BinLog 对应的就是 Coordinator(协调者)节点。当收到请求后,参与者会向协调者发送 PREPARED 或 NO 消息,后者代表了需要回滚。当协调者成功收到所有参与者成功的消息后,会向所有参与者发送 COMMIT 消息,代表事务的提交。
当然还存在着 3PC(三阶段提交)协议,比 2PC 多了一个准备阶段,用于协调者和参与者之间的协商,这里不再展开。
具体的流程如下:
- 协调者向所有参与者(服务A 和 服务B)发送 PREPARE 请求,请求中包含事务 ID。
- 服务A 和 服务B 加锁后确认事务可以在本服务内正常执行,均响应请求。
- 协调者向所有参与者(服务A 和 服务B)发送 COMMIT 命令,请求中包含事务 ID。
- 参与者执行本地事务后解锁,协调者响应请求。
这样存在的问题:一个是锁太大太重,木板效应,所有参与者都会被耗时最长的事务给拖累;耗时长,需要多段网络 IO 才能执行。
Saga
Saga(场景化事务)是一种柔性事务,是现实中分布式事务的一个常用解决方案。其本质就是将分布式事务拆分为多个本地事务,然后通过 message 或 event 异步触发各个本地事务,若执行失败则触发 compensation 逻辑进行补偿。
当然这个操作是存在主体的,也需要一个微服务专门去管理 Saga 的执行。它要求设计者对全局的数据状态有清晰的定义,并定义好各个步骤的补偿逻辑。
在 Sage 中,首先我们会定义一个执行的流程,包含了各个服务执行的顺序,这决定了 MQ 的监听逻辑。随后每一级服务会向 MQ 发送消息,MQ 会监听到消息,并调用对应的服务进行执行,像链条般进行传导。当服务执行错误时,也会调用对应的补偿逻辑,以执行回滚,并且前置服务也会收到消息,逐层反向进行相关的补偿逻辑。
Sage 将分布式事务转化成了多个本地事务,减少了对每个服务的事务锁定,提高了执行的并发度和吞吐量。但这都需要开发者对事务执行的流程有着很清晰的定义,并且需要分别定义好各个服务的补偿逻辑,并且在逐步执行补偿时,也会出现暂时数据不一致的情况。
本地消息表
本地消息表(Local Message Table)是一种常用的分布式事务解决方案,它将分布式事务的各个步骤都存储在本地事务表(如数据库)中,并使用消息队列(Message Queue)来触发各个步骤。
- 服务 A 在执行本地业务操作时,同时将一条消息写入自己的本地事务表中。这两个操作必须在同一个数据库事务中,保证原子性。
- 当本地事务提交后,有一个独立的任务线程会扫描本地事务表,将未发送的消息发送到消息队列。
- 服务 B 订阅并消费这条消息,然后执行自己的本地事务。
- 如果消息发送失败,任务线程会不断重试。如果消息发送成功但服务 B 消费失败,消息队列会负责重试。
这里需要注意的是,既然我们是通过数据库中的一个字段的状态来判断的,那么若服务 B 成功消费,必定得将本地事务表中的状态改为成功,否则就会导致数据不一致。但这里的修改并不能是又 B 主导的回写,因为这个事务和数据库的主体都是 A,若需要修改最终消费成功的状态,必须由 B 会发送消息,让 A 主导来主导修改状态。这样这个状态对用户的披露也能正确被展示了。
我之前在面试当中被问到过,如保证 DB 和 ES 的数据一致性中,我就使用了这样本地消息表的形式来保证数据一致性。但当时面试官就提出质疑了,他说“你先写 DB,并在数据库中留状态字段,再写 ES,成功后发消息更新状态字段。这样不是写了三次了吗,最后一次是冗余的”。
这个质疑是有道理的,跟例子中的本地消息表相比,存在一个最关键的差异,那就是例子中的这个状态比如“订单待支付”->“订单支付成功”的状态,是需要披露给用户显示状态的,而保证双写一致性的状态则是开发者的逻辑,不需要用户知道。因此在这种场景下,本地消息表的冗余写其实并不是最合理的一个解法,使用分布式事务或对账系统来保证数据一致性是一个更好的选择。
对账系统
对账系统是一种牺牲即时强一致性,换取高并发和高可用的,保证最终一致性的解决方案。
实时对账:基于 epoll 监听 binlog 异步写回。
离线对账:利用定时任务进行兜底巡检。
这种方案既可以完全去除代码侵入性,又可以保证数据主体的执行效率。