分布式事务
本地事务
在单体应用中,多个业务操作使用同一条连接操作相同数据库中不同的数据表,一旦有异常可以很容易实现整体回滚。

事务特征
- 原子性:事务中包含的操作要么全部执行,要么全部不执行
- 一致性:事务执行前后,数据库的完整性没有被破坏,即满足所有的约束条件和业务规则
- 隔离性:事务间互不干扰,一个事务的执行不受其他事务的影响,也不影响其他事务。
- 持久性:事务一旦提交,其对数据库的修改就会永久保存,即使发生系统故障或崩溃也不会丢失
隔离级别
Read_Uncommited: 读未提交,允许读取并发事务尚未提交的数据。可能产生脏读、不可重复读、幻读等问题Read_Commited: 读已提交,允许读取并发事务已经提交的数据,可以阻止脏读Repeatable_Read: 可重复读,对同一字段的多次读取结果都一致,可以阻止脏读、不可重复读问题Serializable: 可串行化,所有事务依次执行,可以阻止所有并发问题。
传播行为
Propagation_Required: @Transactional 默认模式,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。Propagation_Requires_New: 总是创建新事务,如果当前存在事务则挂起。开启的事务相互独立,互不干扰Propagation_Nested: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;否则新建事务Propagation_Mandatory: 如果当前存在事务就加入;否则抛出异常
另外还有三种可能不回滚的事务:
Propagation_Supports: 如果当前存在事务就加入;否则以非事务的方式继续运行Propagation_Not_Supported: 以非事务方式运行,如果当前存在事务则把当前事务挂起Propagation_Never: 以非事务方式运行,如果当前存在事务则抛出异常
Spring 管理事务有两种方式:手动硬编码的编程式、基于@Transactional的AOP声明式。
分布式事务
在分布式系统中,多个节点上的多个事务同时执行,这些事务之间可能存在相互依赖的关系。由于网络、硬件等原因,不同节点上的数据可能出现不一致,因此需要分布式事务来保证这些不同服务节点的事务要么全部完成,要么全部失败。

CAP 定理
对于一个分布式系统,在满足 P 的前提下,只能满足 AP(如ZooKeeper)或 CP(如Eureka):
Consistency: 一致性,所有节点访问同一份最新的数据副本Availability:可用性,非故障的节点在合理的时间内返回合理的响应Partition Tolerance:分区容错性,出现网络分区时仍能对外提供服务

原因:若系统出现分区,系统中的某个节点在进行写操作。为了保证 C 一致性, 必须要禁止其他节点的读写操作,这就和 A 可用性发生冲突了。如果为了保证 A 可用性,其他节点的读写操作正常的话,那就和 C 一致性发生冲突了。如果不需要保证 P,即网络分区正常,那么 C 一致性和 A 可用性可以同时保证。
BASE 理论
BASE 理论核心思想是:即使无法做到强一致性,也应采用适当的方式来使系统达到最终一致性。
Basically Available: 基本可用,系统在出现不可预知故障的时候,允许损失部分可用性- 响应时间上的损失:响应时间增加
- 系统功能上的损失:部分用户被导向一个降级页面
Soft State:软状态,允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性(例如允许不同节点的副本同步过程存在延迟)Eventually Consistent:最终一致性,系统中所有的数据副本经过一段时间的同步后,最终达到一致。
BASE 理论是对 CAP 中 一致性C 和 可用性A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。
Paxos
共识算法:通过保持复制日志的一致性,即使面对故障,服务器也可以在共享状态上达成一致。
Paxos 算法是第一个被证明完备的分布式系统共识算法。共识算法的作用是让分布式系统中的多个节点之间对某个提案(Proposal)达成一致的看法。例如哪个节点是 Leader、多个事件发生的顺序等。
Paxos 主要包含两个部分:
1. Basic Paxos
多节点间如何就某个值(提案)达成共识
- Proposer:也称 Coordinator,负责接受客户端的请求并发起提案(提案编号、提议值)
- Acceptor:也称 Voter,负责对提议者的提案进行投票,同时需要记住自己的投票历史
- Learner:如果有超过半数接受者就某个提议达成了共识,那么学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。
2. Multi Paxos
一种思想,通过执行多个 Basic Paxos,就一系列值达成共识。
针对存在恶意节点的情况(拜占庭问题),一般使用 PoW 工作量证明、PoS 权益证明 等公式算法。
Raft
节点类型
一个 Raft 集群包括若干服务器,任何时间点,任意服务器一定会处于以下三个状态中的一个:
- Leader: 负责发起心跳,响应客户端,创建日志,同步日志
- Candidate: Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选
- Follower: 接受 Leader 的心跳和日志同步数据,投票给 Candidate
在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。Follower 是被动的,不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。
选举
任期:Raft 算法将时间划分为任意长度的任期(Redis 中的选举轮次),用连续的数字表示。每个任期都以一次选举开始。
Raft 使用心跳机制来触发 Leader 的选举。
- Leader 通过心跳保持 Leader 状态
- Follower 通过心跳保持为 Follower 状态,并确认 Leader
- 如果 Follower 在一个周期内没收到心跳,将开始一次选举
- 自增自己的 term 并转换为 Candidate,向其他节点请求投票
- 如果超过半数节点投票给自己,则成为 Leader
- 期间,如果收到其它节点声明 Leader 的心跳,且 term 号大于等于自己的 term,则退回 Follower,否则拒绝该请求并让该节点更新
- 如果选举失败,随机一个新的超时时间,之后重新选举
日志复制
- Entry:
<term, index, cmd>的结构,每一个事件成为 entry,只有 Leader 可以创建 entry。 - Log:有 Entry 构成的数组,只有 Leader 才可以改变其他节点的 log。
Leader 收到客户端请求后,会生成一个 entry,添加到自己的日志末尾,并广播给其它服务器,如果收到超过半数的成功响应,标记这个 entry 是 committed 的。Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。
事务方案
- 刚性事务:遵循 ACID 原则,保证强一致性
- 柔性事务:遵循 BASE 理论,保证最终一致性
从实现位置来看,包括基于数据库的 XA 协议(衍生出 2PC、3PC),由事务协调器和本地资源管理器两部分组成,以及基于应用层的 TCC 协议。
2PC
基于数据库支持的 2PC(2 Phase Commit 两阶段提交),顾名思义包括两个阶段:
- 准备阶段:事务协调器要求涉及到事务的每个参与者 prepare 准备执行,并反馈是否可以提交(通过数据库的日志和加锁实现)
- 提交阶段:如果第一阶段每个参与者都准备成功,则要求每个数据库 commit 数据;否则 rollback 回滚

缺点:
- 网络抖动可能造成数据不一致,第二阶段一旦发生网络抖动,导致部分参与者 commit,其它参与者未提交,可能造成数据不一致
- 一旦某个参与者出现延迟,会使得其它参与者占用资源产生阻塞
- 严重依赖协调者,一旦协调者单点故障,可能导致所有参与者处于阻塞
3PC
3PC 三阶段提交是对 2PC 的升级优化,加入了一个准备阶段:
- CanCommit:协调者向所有参与者发送CanCommit命令,询问是否可以执行事务提交操作。如果全部响应YES则进入下一个阶段。
- PreCommit:协调者向所有参与者发送PreCommit命令,询问是否可以进行事务的预提交操作。参与者会执行事务,并暂不提交。一旦失败或者超时,协调者会中断所有事务
- DoCommit:协调者向参与者发送 DoCommit 命令正式提交事务
PS:第三阶段实际上还是会出现部分节点超时的问题吧?
TCC 事务
TCC 指 Try-Confirm-Cancel 补偿事务,是一种业务侵入式较强的事务方案,要求业务处理过程必须拆分为预留业务资源和确认/释放消费资源两个子过程。
- Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好全部需用到的业务资源(保障隔离性)。
- Confirm:确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。Confirm 阶段可能会重复执行,因此本阶段所执行的操作需要具备幂等性。
- Cancel:只要有一个业务方预留资源未成功,则执行取消,释放 Try 阶段预留的业务资源。Cancel 阶段可能会重复执行,也需要满足幂等性。

TCC 不同于 2PC 位于基础设施层面,而是位于业务代码层面,具有较高的灵活性和隔离性,但侵入性较强,每个操作都需要有 try、confirm、cancel 三个接口,且需要保证幂等,开发难度大。
柔性事务 - 最大努力通知
依赖持续重试来保证可靠性,提供可查询操作接口进行核对。
柔性事务 - 可靠消息+最终一致性
业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只 记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确 认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
- TC - Transaction Coordinator 事务协调者,维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM - Transaction Manager 事务管理器,定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM - Resource Manager 资源管理器,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
MQ 实现最终一致性
业务场景:当用户下单时,订单系统生成订单,商品系统扣减库存,促销系统扣减优惠券,最后订单入库。 虽然可以用定时任务轮询数据库完成业务需求,但消耗系统内存、增加数据库压力、时间误差大等问题,因此结合 MQ 的延迟队列和死信队列实现系统的最终一致性。

订单业务
用户下单后,订单系统需要
- 发送订单创建的消息给 MQ,并设置 TTL,经过设定的 TTL 后进入死信队列,监听死信队列的服务根据订单的状态(已支付/未支付)进行后续业务处理(交付仓储物流/关单库存解锁),实现自动关单
- 然后进入库存系统锁定库存
库存锁定
- 由于订单可能回滚,所以为了能够得到库存锁定的信息,在锁定时需要记录库存工作单(订单信息、商品信息、数量等)
- 库存保存在 Redis 中,收到请求后判断库存是否充足,然后减掉 Redis 中的库存(Redis 中的库存扣完就无法下单了)
- 锁定成功后向延迟队列发送消息(锁定的相关信息),实现库存自动解锁。然后修改对应的订单状态,进入支付流程
- 支付成功后订单进入出库状态,此时扣除数据库中的库存。如果中途失败,则取消订单,解锁库存等
库存自动解锁:库存锁定(Redis 中)后,也会有一个消息发送到延迟队列,经过 TTL 后根据订单状态和工作单状态来判断是否进行库存解锁,只有订单是过期的,且工作单的库存处于锁定状态才进行库存解锁。
自动关单和自动解锁库存两个业务通过重新查询当前的状态来保证幂等性。
优惠券业务
- 订单系统下单时,将扣减的优惠券事件放入消息队列中,最终优惠券系统会执行对应的业务,完成后通过手动确认的方式发送应答,告诉 MQ 删除这条消息,防止业务出现异常而消息已经被删除的问题。
- 同时订单系统需要将对应的优惠券消息写入数据库,并把状态设为未完成,优惠券系统在消费成功后,也向 MQ 发送一个消息,通知订单系统将这个订单的优惠券业务状态设为完成,这样,即使消息积压导致消息丢失,也能通过定时任务将未完成的消息重新发送,实现最终一致性。