[ 秒杀项目必问 ] 分布式事务详解

分布式事务

引入

考虑实现一个秒杀功能, 为了避免超卖, 秒杀服务自己先立即扣减库存(自己操作数据库), 然后调用订单服务生成订单(订单服务操作数据库);

发生一次秒杀时, 我们希望这两个服务的业务逻辑要么都执行, 要么都不执行;

假设两个服务部署在不同的机器上, 连接到同一个 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请求,驱动参与者执行本地事务。

  1. 协调者发送准备请求:
    • 协调者向所有参与者发送prepare请求,询问是否可以准备提交事务。
    • 协调者等待所有参与者的响应。
  2. 参与者响应准备请求:
    • 每个参与者执行本地事务操作,但不提交
    • 如果参与者可以提交事务,则返回vote-commit给协调者。
    • 如果参与者不能提交事务(例如遇到错误或冲突),则返回vote-abort给协调者。

所有参与者响应后, 进入提交阶段,根据参与者的响应,协调者决定是否提交或回滚事务。

  1. 协调者决定提交或回滚:
    • 如果所有参与者都返回vote-commit,协调者发送commit消息给所有参与者,指示它们提交事务。
    • 如果有任何参与者返回vote-abort,协调者发送abort消息给所有参与者,指示它们回滚事务。
  2. 参与者提交或回滚事务:
    • 如果接收到commit消息,参与者提交事务并释放资源。
    • 如果接收到abort消息,参与者回滚事务并释放资源。

可以看出, 当前处于两阶段中的哪个阶段, 数据库是可以感知到的; 对比后面的 TCC , TCC也是两阶段, 但是是在业务层面实现的两阶段, 数据库无感;

2PC缺陷

优点就是强一致, 实现简单, 这里说一下缺点;

  1. 阻塞问题
    • 参与者在准备阶段会锁定数据库中自己所涉及的记录, 到参与者提交成功才会释放;
    • 这期间要等待协调者收集所有参与者的响应并进行决策, 这可能导致数据库资源长时间被锁定;
  2. 性能开销
    • 两阶段提交协议需要多次网络通信和等待参与者的响应,导致性能开销较大。
  3. 数据库支持:
    • 需要数据库支持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框架自动调用;

流程

在这里插入图片描述

  1. 开启分布式事务(使用注解等方式, 框架会自动开启事务)
  2. 在方法内部由开发者自己去调用不同服务的 Try 接口; 然后开发者就可以不用管了;
  3. 如果任意一个 Try 失败, 协调器自动调用所有成功服务的 Cancel 接口;
  4. 如果所有 Try 都成功, 协调器自动调用所有服务的 Confirm 接口;

重试

如果在二阶段调用 Confirm 或者 Cancel 因为网络等原因出现失败的情况, 那么会不断地进行重试;

所以 Confirm 和 Cancel 都需要具有幂等性;

对比2PC

  1. TCC 不需要数据库支持两阶段协议, 是业务层面的两阶段, 非关系型数据库也能用, 甚至跨数据库也没关系; 而 2PC 需要数据库支持;
  2. 不会导致资源长时间被锁定, 性能好;

缺陷

  1. 不是强一致;
  2. 分布式事务的代码侵入业务, 耦合性很强, 一旦业务逻辑发生改变, 分布式事务代码也需要改变;
  3. Confirm 和 Cancel 需要实现幂等性, 比较复杂;

柔性事务之本地消息表

借助于消息队列和本地消息表, 实现分布式事务;

在这里插入图片描述

一致性保证

如果本地消息表中一直是 (库存已扣减, 订单未生成), 就会一直发生成订单的消息给订单服务, 由此来尽力保证都提交;

如果生成订单失败, 怎么保证都失败? 可以对已经生成超过一定时间的(库存已扣减, 订单未生成) 的消息记录, 放弃发送生成消息, 并且根据消息内容添加库存;

缺陷

需要扫描本地消息表, 随着分布式事务的累计, 本地消息表会越来越大, 扫描也越来越慢, 不适合高并发的场景;

需要由消息队列保证消息投递的可靠性;

柔性事务之RocketMQ

事务消息 | RocketMQ (apache.org)

实现了一个两阶段提交的消息机制:

  • Sender, 即上游服务, Subscriber, 即下游服务;
  • Server, 即消息队列;
  • LocalTransaction 为上游服务的本地事务;

在这里插入图片描述

过程

  1. 上游服务Half消息(半事务消息) 发送至消息队列

  2. 消息队列将消息持久化成功之后,向上游服务返回Ack确认消息已经发送成功,此时 Half消息 被标记为"暂不能投递"

  3. 上游服务收到确认之后, 开始执行本地事务逻辑。

  4. 上游服务根据本地事务执行结果向服务端提交 Commit 或 Rollback 消息(称为二次确认)消息队列收到后:

    • 如果是 Commit:消息队列将半事务消息标记为可投递,并投递给下游服务, 相当于把这条消息 Commit 了。
    • 如果是 Rollback:消息队列不会投递半事务消息, 相当于把这条消息 Rollback了。
  5. 消息队列Half消息投递给下游服务, 并且这个投递会基于 RocketMQ 的重试机制;
    消费重试 | RocketMQ (apache.org)

  6. 消息队列超过一定时间 ( 回查的间隔时间 ) 未收到上游服务提交的二次确认结果,消息队列将对上游服务的一个实例发送回查消息。回查的间隔时间和最大回查次数,都可以配置;

  7. 上游服务收到回查消息后,需要检查对应消息的本地事务执行的最终结果。

  8. 上游服务根据检查到的本地事务的最终状态提交二次确认消息队列对其进行处理。

一致性保证

分情况说明:

  1. 上面描述的过程中每一步都正常执行了, 那么分布式事务内的所有操作, 全部执行;

  2. 如果消息队列一开始就挂了, 上游服务收不到 Half 消息的确认, 就不会开启本地事务, 都不执行;

  3. 上游服务的本地事务执行失败, 本地事务回滚并发送 Rollback 二次确认, 都不执行;

  4. 如果二次确认发送失败, 会有回查机制进行重试;

    如果回查也一直失败, 比如网络原因, 导致超过半事务消息最大超时时长, RocketMQ直接回滚半事务消息, 不会投递;

    这时候下游服务没执行; 上游服务的事务到底执行没执行, 消息队列并不知道, 只知道你没给我二次确认, 这时应该怎么处理上游服务?

    答: 超过半事务消息超时时长后, 可以进行邮件通知之类的操作, 由程序员决定如何处理;

  5. 如果消息投递给下游服务, 没有收到下游服务的消费确认, 会由RocketMQ的重试机制和死信机制保证最终成功消费(要保证消费者一方的幂等性), 实现最终一致性;

Alibaba 的分布式事务组件 Seata 实现了以上提到的 2PC 事务, TCC 事务; 其独有的 AT 事务实现方式也有很多考点, 将在后续的文章中详细介绍 Seata 的部署与使用

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值