分布式事务
引入
考虑实现一个秒杀功能, 为了避免超卖, 秒杀服务自己先立即扣减库存(自己操作数据库), 然后调用订单服务生成订单(订单服务操作数据库);
发生一次秒杀时, 我们希望这两个服务的业务逻辑要么都执行, 要么都不执行;
假设两个服务部署在不同的机器上, 连接到同一个 Mysql 数据库;
两个服务都要和数据库建立连接, 使用各自的连接开启事务, 并完成自己的业务逻辑;
回顾数据库事务: 一个连接发送start transaction
后, 开启一个事务, 后续这个连接发送的请求, 直到rollback 或 commit
之前, 都属于同一个事务;
如果这个连接上的当前事务还没结束, 这个连接又发起了一次start transaction
, 那么会自动提交这个连接的当前事务, 然后在这个连接上再开一个新的事务;
不同连接上的SQL, 肯定属于不同的事务;
所以秒杀服务和订单服务使用的根本不是同一个事务, 无法保证两个服务要么都执行成功, 要么都执行失败;
在其中一个服务上添加 @Transactional
注解, 只能保证这个服务自己的事务错误回滚;
所以要引入 "分布式事务" 的概念, 使得事务可以跨节点; 与之相对的, 称单个单个结点上的事务为本地事务, 也叫分支事务;
本地事务
要在 SpringCloud 环境下使用本地事务
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
需要导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
如果已经导入 mybatis 则已经包含了这个依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
CAP理论
CAP理论是分布式系统领域的一个基本概念,描述了分布式数据存储系统在设计和实现中的三个核心属性:一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)。
一致性(Consistency):
- 同一时间, 从系统不同结点读取同一份数据, 结果应该是相同的;
- 一致性保证了数据在多个节点之间是同步的,无论从哪个节点读取数据,结果都是相同的。
可用性(Availability):
- 每次请求都能收到响应。即使有部分节点发生故障,系统仍然能够响应请求。
- 可用性保证了系统始终可用,并且总是可以接收并处理请求。
分区容错性(Partition Tolerance):
- 即使在网络分区(节点之间的网络通信失败)的情况下系统能够继续运行并响应请求。
- 分区容错性保证了系统能够处理结点之间通信失败带来的问题,继续运行并维持服务可用。
CAP定理的核心结论是,在一个分布式数据存储系统中,最多只能同时满足三个特性中的两种。
CP(Consistency and Partition Tolerance):
- 系统在网络分区时保持一致性,但可能牺牲可用性。
- 当网络分区发生时,为了保证一致性,系统可能拒绝请求,导致部分服务不可用。
- Zookeeper的服务注册中心( 大数据领域, 管理 Hadoop, Hive...的工具, 提供分布式配置服务, 同步服务) 就是 CP 系统
AP(Availability and Partition Tolerance):
- 系统在网络分区时保持可用性,但可能牺牲一致性。
- 当网络分区发生时,系统仍然会响应请求,但可能导致不同节点的数据不一致。
- Eureka, Redis就是AP系统;
CA(Consistency and Availability):
- 系统在正常运行时保持一致性和可用性,但一出现网络分区行为就不可预期。
- 因为在广域网(如互联网)中,网络分区是不可避免的, 所以分布式环境中不考虑 CA 系统;
刚性事务
定义: 追求数据的强一致性, 遵循事务的 ACID 原则, 遵循 CP 模型;
强一致的思想是各个分支事务执行完不要提交, 等待所有分支事务都有了结果, 再统一提交或回滚;
代表: 两阶段提交 2PC;
2PC
X/Open组织定义了解决分布式事务问题的一套规范, 即XA规范; 2PC协议实现了XA规范;
它通过将事务分为两个阶段来协调多个节点,使所有参与节点要么全部提交事务,要么全部回滚事务。
由一个协调者(Coordinator)和多个参与者(Participants)组成。
协调者负责管理事务的提交过程,确保所有参与者都达成一致。
协议分为两个阶段:准备阶段(Prepare Phase)和提交阶段(Commit Phase)。
2PC流程
在准备阶段,协调者向所有参与者发送prepare
请求,驱动参与者执行本地事务。
- 协调者发送准备请求:
- 协调者向所有参与者发送
prepare
请求,询问是否可以准备提交事务。 - 协调者等待所有参与者的响应。
- 参与者响应准备请求:
- 每个参与者执行本地事务操作,但不提交
- 如果参与者可以提交事务,则返回
vote-commit
给协调者。 - 如果参与者不能提交事务(例如遇到错误或冲突),则返回
vote-abort
给协调者。
所有参与者响应后, 进入提交阶段,根据参与者的响应,协调者决定是否提交或回滚事务。
- 协调者决定提交或回滚:
- 如果所有参与者都返回
vote-commit
,协调者发送commit
消息给所有参与者,指示它们提交事务。 - 如果有任何参与者返回
vote-abort
,协调者发送abort
消息给所有参与者,指示它们回滚事务。
- 参与者提交或回滚事务:
- 如果接收到
commit
消息,参与者提交事务并释放资源。 - 如果接收到
abort
消息,参与者回滚事务并释放资源。
可以看出, 当前处于两阶段中的哪个阶段, 数据库是可以感知到的; 对比后面的 TCC , TCC也是两阶段, 但是是在业务层面实现的两阶段, 数据库无感;
2PC缺陷
优点就是强一致, 实现简单, 这里说一下缺点;
- 阻塞问题:
- 参与者在准备阶段会锁定数据库中自己所涉及的记录, 到参与者提交成功才会释放;
- 这期间要等待协调者收集所有参与者的响应并进行决策, 这可能导致数据库资源长时间被锁定;
- 性能开销:
- 两阶段提交协议需要多次网络通信和等待参与者的响应,导致性能开销较大。
- 数据库支持:
- 需要数据库支持2PC协议, 常用的关系型数据库基本支持, 非关系型数据库基本不支持;
BASE理论
接近于CAP理论中的 AP, 强调系统的可用性和一致性, 但是采取了一种比较宽松的策略, 只要保证基本可用和最终一致;
与CAP理论相比,BASE理论更注重实际应用和用户体验,通过容忍短暂的不一致来实现系统的高可用性和分区容错性。
将基于BASE理论的分布式事务解决方案称为柔性事务;
基本可用(Basically Available):
- 系统保证在大多数情况下是可用的,但不需要保证完全可用。
- 在出现部分故障或网络分区时,系统可以降级服务,提供部分功能或延迟响应,而不是完全不可用。
软状态(Soft State):
- 系统中的状态可以在不影响整体系统运行的情况下变化,即状态不需要总是保持一致。
- 允许数据在不同节点之间存在暂时的不一致。
最终一致性(Eventual Consistency):
- 系统保证在没有新的更新操作后,经过一段时间,所有节点的数据最终会达到一致。
- 不要求强一致性(每次读取都能获得最新的数据),而是允许读取到旧的数据,但保证在一定时间内数据会完成同步。
基于BASE理论实现的分布式事务解决方案称为柔性事务, 追求的是最终一致性, 允许暂时存在不一致的情况;
柔性事务之TCC
简介
同样是两阶段提交, 但是把两阶段拿到业务层面, 不需要数据库支持;
Seata 就有 TCC 模式的实现方案;
划分出三个阶段或者说接口 Try, Confirm, Cancel
, 参与分布式事务的业务需要实现这三个接口。
Try(尝试阶段):
- 在这个阶段,每个参与者执行初步操作,预留必要的资源;
- 如果任何一个参与者的 Try 操作失败,整个分布式事务将不会进入Confirm阶段,而是直接进入Cancel阶段。
Confirm(确认阶段):
- 如果所有参与者的Try操作都成功,分布式事务进入Confirm阶段。
- 在这个阶段,每个参与者完成业务操作,将预留的资源进行正式的变更。
- 因为有重试机制, 所以 Confirm 操作必须是幂等的
Cancel(取消阶段):
- 如果任何一个参与者的Try操作失败,事务进入Cancel 阶段。
- 在这个阶段,每个参与者撤销Try阶段的预留操作,释放预留的资源。
- 因为有重试机制, Cancel操作也必须是幂等的。
举例
假设一个转账请求, A账户转账给B账户100元; 由增加余额和扣减余额两个服务实现; 如果没有分布式事务, 两个服务不能保证都成功或都失败, 可能出现扣减成功但增加失败的情况;
在账户表中设置一个额外的字段, 冻结资金;
在 Try 阶段中, 扣减服务检查A账户是否存在, 检查A账户余额是否够100, 修改A账户的冻结资金为100, 将A账户余额扣减100;
增加服务检查B账户是否存在, 修改 B 账户冻结资金为100;
这几步操作任意一步失败, 都认为 Try 失败;
在 Confirm 阶段, 扣减服务将账户 A 的冻结余额置零, 增加服务将账户 B 的冻结资金加到余额中;
Cancel 阶段, 扣减服务将冻结金额加回到 A 的账户余额, 增加服务将 B 的冻结资金清零;
Try, Confirm, Cancel 的逻辑, 都由开发者自己实现;
Try 由开发者自己调用, Confirm 和 Cancel 由TCC框架自动调用;
流程
- 开启分布式事务(使用注解等方式, 框架会自动开启事务)
- 在方法内部由开发者自己去调用不同服务的 Try 接口; 然后开发者就可以不用管了;
- 如果任意一个 Try 失败, 协调器自动调用所有成功服务的 Cancel 接口;
- 如果所有 Try 都成功, 协调器自动调用所有服务的 Confirm 接口;
重试
如果在二阶段调用 Confirm 或者 Cancel 因为网络等原因出现失败的情况, 那么会不断地进行重试;
所以 Confirm 和 Cancel 都需要具有幂等性;
对比2PC
- TCC 不需要数据库支持两阶段协议, 是业务层面的两阶段, 非关系型数据库也能用, 甚至跨数据库也没关系; 而 2PC 需要数据库支持;
- 不会导致资源长时间被锁定, 性能好;
缺陷
- 不是强一致;
- 分布式事务的代码侵入业务, 耦合性很强, 一旦业务逻辑发生改变, 分布式事务代码也需要改变;
- Confirm 和 Cancel 需要实现幂等性, 比较复杂;
柔性事务之本地消息表
借助于消息队列和本地消息表, 实现分布式事务;
一致性保证
如果本地消息表中一直是 (库存已扣减, 订单未生成), 就会一直发生成订单的消息给订单服务, 由此来尽力保证都提交;
如果生成订单失败, 怎么保证都失败? 可以对已经生成超过一定时间的(库存已扣减, 订单未生成) 的消息记录, 放弃发送生成消息, 并且根据消息内容添加库存;
缺陷
需要扫描本地消息表, 随着分布式事务的累计, 本地消息表会越来越大, 扫描也越来越慢, 不适合高并发的场景;
需要由消息队列保证消息投递的可靠性;
柔性事务之RocketMQ
实现了一个两阶段提交的消息机制:
- Sender, 即上游服务, Subscriber, 即下游服务;
- Server, 即消息队列;
- LocalTransaction 为上游服务的本地事务;
过程
上游服务
将Half消息(半事务消息)
发送至消息队列
。消息队列
将消息持久化成功之后,向上游服务
返回Ack确认消息已经发送成功,此时Half消息
被标记为"暂不能投递"上游服务
收到确认之后, 开始执行本地事务逻辑。上游服务
根据本地事务执行结果向服务端提交Commit 或 Rollback 消息(称为二次确认)
,消息队列
收到后:
- 如果是 Commit:
消息队列
将半事务消息标记为可投递,并投递给下游服务
, 相当于把这条消息 Commit 了。 - 如果是 Rollback:
消息队列
不会投递半事务消息, 相当于把这条消息 Rollback了。
消息队列
将Half消息
投递给下游服务, 并且这个投递会基于 RocketMQ 的重试机制; 消费重试 | RocketMQ (apache.org)- 若
消息队列
超过一定时间 ( 回查的间隔时间 ) 未收到上游服务
提交的二次确认结果,消息队列
将对上游服务
的一个实例发送回查消息
。回查的间隔时间和最大回查次数,都可以配置; 上游服务
收到回查消息
后,需要检查对应消息的本地事务执行的最终结果。上游服务
根据检查到的本地事务的最终状态提交二次确认
,消息队列
对其进行处理。
一致性保证
分情况说明:
- 上面描述的过程中每一步都正常执行了, 那么分布式事务内的所有操作, 全部执行;
- 如果消息队列一开始就挂了, 上游服务收不到 Half 消息的确认, 就不会开启本地事务, 都不执行;
- 上游服务的本地事务执行失败, 本地事务回滚并发送
Rollback
二次确认, 都不执行; - 如果二次确认发送失败, 会有回查机制进行重试;
如果回查也一直失败, 比如网络原因, 导致超过半事务消息最大超时时长, RocketMQ直接回滚半事务消息, 不会投递;
这时候下游服务没执行; 上游服务的事务到底执行没执行, 消息队列并不知道, 只知道你没给我二次确认, 这时应该怎么处理上游服务?
答: 超过半事务消息超时时长后, 可以进行邮件通知之类的操作, 由程序员决定如何处理; - 如果消息投递给下游服务, 没有收到下游服务的消费确认, 会由RocketMQ的重试机制和死信机制保证最终成功消费(要保证消费者一方的幂等性), 实现最终一致性;