文章

面试题标准答案 如何解决分布式事务问题的?

面试题标准答案 如何解决分布式事务问题的?

面试题标准答案: 如何解决分布式事务问题的?

现在Java面试,分布式系统、分布式事务几乎是标配。而分布式系统、分布式事务本身比较复杂,大家学起来也非常头疼。

```plain text 面试题:分布式事务了解吗?你们是如何解决分布式事务问题的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Seata AT模式和Seata TCC是在生产中最常用。

- 强一致性模型,Seata AT **强一致方案** 模式用于强一致主要用于核心模块,例如交易/订单等。
- 弱一致性模型。Seata TCC **弱一致方案**一般用于边缘模块例如库存,通过TC的协调,保证最终一致性,也可以业务解耦。

面试中如果你真的被问到,可以分场景回答:

### (1)强一致性场景

对于那些特别严格的场景,用的是Seata AT模式来保证强一致性;

准备好例子:你找一个严格要求数据绝对不能错的场景(如电商交易交易中的库存和订单、优惠券),可以回答使用成熟的如中间件Seata AT模式。

```plain text
阿里开源了分布式事务框架seata经历过阿里生产环境大量考验的框架。  seata支持Dubbo,Spring Cloud。

是Seata AT模式,保障强一致性,支持跨多个库修改数据;

  • 订单库:增加订单
  • 商品库:扣减库存
  • 优惠券库:预扣优惠券

(2)弱一致性场景

对于数据一致性要求没有那些特别严格、或者由不同系统执行子事务的场景,可以回答使用Seata TCC 保障弱一致性方案

准备好例子:一个不是严格对数据一致性要求、或者由不同系统执行子事务的场景,如电商订单支付服务,更新订单状态,发送成功支付成功消息,只需要保障弱一致性即可。

Seata TCC 模式,保障弱一致性,支持跨多个服务和系统修改数据,在上面的场景中,使用Seata TCC 模式事务

  • 订单服务:修改订单状态
  • 通知服务:发送支付状态

(3)最终一致性场景

基于可靠消息的最终一致性,各个子事务可以较长时间内异步,但数据绝对不能丢的场景。可以使用异步确保型事务事。

可以使用基于MQ的异步确保型事务,比如电商平台的通知支付结果:

  • 积分服务:增加积分
  • 会计服务:生成会计记录

各大模式的总体对比:

属性2PCTCCSaga异步确保型事务尽最大努力通知
事务一致性
复杂性
业务侵入性
使用局限性
性能
维护成本

参考资料

https://blog.csdn.net/wuzhiwei549/article/details/80692278

https://www.i3geek.com/archives/841

https://www.cnblogs.com/seesun2012/p/9214653.html

https://github.com/yangliu0/DistributedLock

https://www.cnblogs.com/liuyang0/p/6744076.html

https://www.cnblogs.com/liuyang0/p/6800538.html

https://mwhittaker.github.io/blog/an_illustrated_proof_of_the_cap_theorem/

https://www.infoq.cn/article/cap-twelve-years-later-how-the-rules-have-changed

https://www.cnblogs.com/bluemiaomiao/p/11216380.html

https://www.jianshu.com/p/d909dbaa9d64

https://book.douban.com/subject/26292004/

https://segmentfault.com/a/1190000004468442

https://blog.csdn.net/universsky2015/article/details/105727244/

https://www.cnblogs.com/wudimanong/p/10340948.html

https://blog.csdn.net/kusedexingfu/article/details/103484198

https://www.jianshu.com/p/bfb619d3eea2

http://seata.io/zh-cn/docs/dev/mode/at-mode.html

https://blog.csdn.net/kusedexingfu/article/details/103484198

https://blog.csdn.net/wsdc0521/article/details/108223310

https://blog.csdn.net/lidatgb/article/details/38468005

https://www.cnblogs.com/cuiqq/p/12175826.html

https://blog.csdn.net/qq_22343483/article/details/99638554

https://blog.csdn.net/SOFAStack/article/details/99670033

https://seata.io/zh-cn/docs/dev/mode/at-mode.html

http://t.zoukankan.com/lay2017-p-12528071.html

https://segmentfault.com/a/1190000037757622

https://blog.csdn.net/qq_31960623/article/details/116429261

并入整理

以下内容由同主题重复文章合并而来,便于集中复习。

Seata 面试题

以下内容,部分整理自 Seata 官网

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 为用户提供了 ATTCCSAGAXA 事务模式,为用户打造一站式的分布式解决方案。

Seata 简介

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

  • 官网: seata.io
  • GitHub: github.com/seata/seata (超过 20k stars)
  • 核心功能: 提供 ATTCCSAGAXA 四种事务模式。
  • 背景: 在开源之前,Seata 是阿里巴巴内部的分布式一致性中间件,成功支撑了历年“双十一”等复杂场景。其商业化产品为阿里云上的 GTS

相关链接:

Seata 的三大核心模块

Seata 的架构主要由三个核心角色组成:

  1. TC (Transaction Coordinator) - 事务协调者
    • 职责:作为中心化的服务端,负责生成全局唯一的事务 ID,对全局事务进行注册、提交或回滚。
  2. TM (Transaction Manager) - 事务管理器
    • 职责:作为客户端 SDK 嵌入在事务发起方应用中,负责定义全局事务的边界(开始、提交、回滚)。
  3. RM (Resource Manager) - 资源管理器
    • 职责:作为客户端 SDK 嵌入在参与事务的各个资源服务中,负责管理分支事务,并与 TC 通信以汇报状态和执行指令。

在 Seata 中,TMRM 作为客户端与业务系统集成,而 TC 是一个独立部署的服务端。

Seata 分布式事务的执行流程

Seata 的分布式事务遵循一个标准的二阶段提交流程:

  1. TM 请求 TC 开启一个全局事务,TC 生成并返回一个全局事务 ID (XID)。
  2. 各个 RM 在执行本地业务逻辑时,向 TC 注册分支事务,并汇报资源准备状态。
  3. TM 在业务逻辑执行完毕后,通知 TC 结束全局事务(触发全局提交或回滚),这是一阶段的结束。
  4. TC 汇总所有分支事务的状态,决定全局事务是提交还是回滚。
  5. TC 通知所有 RM 执行最终的提交或回滚操作,这是二阶段的结束。

Seata 事务流程

这个模型与经典的 X/Open DTP (Distributed Transaction Process) 模型非常相似。

DTP 模型角色:

  • AP (Application): 应用程序,定义事务边界。
  • TM (Transaction Manager): 事务管理器,协调全局事务。
  • RM (Resource Manager): 资源管理器,通常是数据库。

DTP 模型

Seata 的四种事务模式

Seata 提供了四种不同的分布式事务解决方案以适应不同场景:

  • AT (Automatic Transaction) 模式
  • TCC (Try-Confirm-Cancel) 模式
  • Saga 模式
  • XA 模式

Seata AT 模式详解

AT (Automatic Transaction) 模式 是 Seata 最早支持且最常用的模式,它是一种基于增强型两阶段提交协议的演进方案,核心优势在于对业务代码的侵入性极低。

AT 模式的使用前提

  1. 基于支持本地 ACID 事务的关系型数据库 (例如,使用 InnoDB 存储引擎的 MySQL)。
  2. Java 应用,通过 JDBC 访问数据库。

AT 模式核心机制

AT 模式是对传统两阶段提交(2PC)的优化:

  • 一阶段:业务数据和回滚日志(undo_log)在同一个本地事务中提交,然后立即释放本地锁和数据库连接资源。
  • 二阶段
    • 如果全局提交TC 会异步通知各 RM 清理对应的 undo_log,这个过程非常快。
    • 如果全局回滚RM 会根据一阶段记录的 undo_log 生成反向补偿操作来恢复数据。

Seata AT 模型图

AT 模式工作流程示例(充值业务)

假设一个充值业务需要调用“余额服务”和“积分服务”。

第一阶段流程

第一阶段流程图

  1. 余额服务的 TMTC 申请开启全局事务,获取 XID。
  2. 余额服务的 RMTC 注册分支事务。
  3. 余额服务在本地事务中执行业务 SQL,并生成 undo_log,然后提交本地事务。
  4. 余额服务的 RMTC 汇报分支事务执行成功。
  5. 余额服务通过 RPC 调用积分服务,并传递 XID。
  6. 积分服务的 RMTC 注册分支事务。
  7. 积分服务在本地事务中执行业务 SQL,并生成 undo_log,然后提交本地事务。
  8. 积分服务的 RMTC 汇报分支事务执行成功。
  9. 积分服务返回调用成功。
  10. 余额服务的 TMTC 请求全局提交。

第二阶段流程

  • 全局提交TC 异步通知所有 RM 删除对应的 undo_log 记录。
  • 全局回滚TC 通知所有 RM 根据 undo_log 进行数据回滚。回滚前,RM 会校验当前数据与 undo_log 中的“后镜像 (afterImage)”是否一致,以防止数据被其他事务修改(避免脏写)。

Seata AT 模式在电商下单场景的应用

假设一个电商下单场景:shopping-service 调用 repo-service 扣减库存,然后调用 order-service 创建订单。

电商场景

工作流程详述

  1. shopping-service (TM) 向 TC 注册全局事务,获取 XID。
  2. repo-service (RM) 执行本地事务:
    • 锁定库存记录。
    • 生成 undo_log(记录数据修改前后的镜像)。
    • 执行 UPDATE 语句扣减库存。
    • 提交本地事务,释放本地锁。
    • TC 注册分支事务并汇报成功。
  3. order-service (RM) 执行本地事务:
    • 锁定订单表。
    • 生成 undo_log
    • 执行 INSERT 语句创建订单。
    • 提交本地事务,释放本地锁。
    • TC 注册分支事务并汇报成功。
  4. TC 收到所有分支事务成功的信息,决定全局提交。
  5. TC 通知所有 RM 异步删除 undo_log

如果任何一个分支事务失败,TC 将决定全局回滚,并通知所有 RM 根据 undo_log 恢复数据。

undo_log 表结构

undo_logAT 模式实现自动回滚的关键。

undo_log 表结构

  • xid: 全局事务 ID。
  • branch_id: 分支事务 ID。
  • rollback_info: 记录了数据修改前后的镜像(beforeImageafterImage)。

Seata 的数据隔离性

写隔离

AT 模式通过 全局锁 (Global Lock) 来保证写隔离。

  • 流程
    1. 一个分支事务在执行本地事务前,必须先获取本地锁。
    2. 在提交本地事务前,必须成功获取该记录的 全局锁
    3. 如果获取 全局锁 失败,则回滚本地事务,释放本地锁,并进行重试。
    4. 全局事务结束后,释放 全局锁
  • 结论:由于 全局锁 的存在,一个全局事务在完成之前会一直持有锁,从而阻止其他事务对同一记录的修改,避免了“脏写”问题。

写隔离-成功流程

写隔离-回滚流程

读隔离

  • 默认隔离级别:在数据库本地事务隔离级别为“读已提交” (Read Committed) 的基础上,Seata AT 模式默认的全局隔离级别是 “读未提交” (Read Uncommitted)。这意味着一个事务可能读取到另一个尚未提交的全局事务修改的数据。

  • 如何实现“读已提交”:如果业务需要全局的“读已提交”,可以使用 SELECT ... FOR UPDATE 语句。

    • 当执行 SELECT FOR UPDATE 时,Seata 的 RM 会尝试获取该记录的 全局锁
    • 如果 全局锁 被其他事务持有,查询将被阻塞,直到获取到锁为止。
    • 这样可以确保查询操作读取到的是已提交的数据。

读隔离

Spring Cloud 集成 Seata AT 模式

集成步骤非常简单:

  1. 在项目中引入 Seata 依赖,并配置 Seata Server 地址。
  2. 在每个参与事务的微服务数据库中创建 undo_log 表。
  3. 在事务发起方(主服务)的方法上添加 @GlobalTransactional 注解。
  4. 确保数据源被 Seata 的 DataSourceProxy 代理。

Seata TCC 模式

TCC (Try-Confirm-Cancel) 模式是另一种两阶段事务模型,与 AT 模式相比:

  • 优点:性能更高,不依赖数据库的本地事务,不使用 全局锁,允许更高的并发。
  • 缺点:对业务代码侵入更强,需要为每个分支事务手动实现 TryConfirmCancel 三个方法。

TCC 模式

TCC 的三个阶段

  1. 一阶段:Try
    • 对业务资源进行检查和预留。例如,在扣减账户余额时,不是直接扣减,而是将一部分金额“冻结”。
  2. 二阶段:Confirm
    • 如果全局事务提交,则执行真正的业务操作。例如,将“冻结”的金额实际扣除。
    • Confirm 方法必须是幂等的。
  3. 二阶段:Cancel
    • 如果全局事务回滚,则取消一阶段预留的资源。例如,将“冻结”的金额“解冻”。
    • Cancel 方法必须是幂等的。

TCC 流程


Seata Saga 模式

Saga 模式是 SEATA 提供的长事务解决方案。它将一个长事务拆分为多个本地事务,每个本地事务都有一个对应的补偿操作。

  • 正向执行:依次执行每个本地事务。
  • 反向补偿:如果某个本地事务失败,则依次调用前面已成功事务的补偿操作。

Saga 模式

适用场景

  • 业务流程长、涉及多个参与者。
  • 参与者可能是遗留系统或外部服务,无法提供 TCC 模式所需的接口。

优缺点

  • 优点
    • 一阶段提交本地事务,无锁,性能高。
    • 事件驱动,可异步执行,吞吐量高。
    • 补偿服务易于实现。
  • 缺点
    • 不保证隔离性。

Seata 通过内置的状态机引擎来实现 Saga 模式,开发者可以通过 JSON 格式的状态图来编排服务调用和补偿流程。


Seata XA 模式

XA 模式是 Seata 对传统 XA 规范的支持。它利用数据库本身对 XA 协议的支持来管理分支事务。

使用前提

  • 数据库必须支持 XA 事务(如 MySQL、Oracle)。
  • Java 应用通过 JDBC 访问数据库。

工作机制

XA 模式将 Seata 的 TC 作为 XA 规范中的“事务协调者”,而由数据库驱动实现的 XA 资源则作为“资源管理器”。

XA 模式机制

在 Seata 中使用 XA 模式,开发者只需将数据源代理从 DataSourceProxy (AT 模式) 更换为 DataSourceProxyXA (XA 模式) 即可,上层编程模型保持不变。

```java @Bean(“dataSource”) public DataSource dataSource(DruidDataSource druidDataSource) { // AT 模式的数据源代理 // return new DataSourceProxy(druidDataSource);

1
2
// XA 模式的数据源代理
return new DataSourceProxyXA(druidDataSource); }

什么是BASE理论?

什么是BASE理论

由于不能同时满足CAP,所以出现了BASE理论:BA:Basically Available,表示基本可用,表示可以允许一定程度的不可用,比如由于系统故障,请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的。S:Soft state:表示分布式系统可以处于一种中间状态,比如数据正在同步E:Eventually consistent,表示最终一致性,不要求分布式系统数据实时达到一致,允许在经过一段时间后再达到一致,在达到一致过程中,系统也是可用的

什么是三阶段协议?

这个问题,严格来说不属于【分布式事务】相关,考虑到本文已经出现了一阶段提交、二阶段提交,所以这里就瞬时“硬塞”一个三阶段提交。

感兴趣的胖友,可以看看 《数据库 分布式事务 2阶提交 3阶提交》 文章。

事务解决方案的对比总结

总的来说,TCC 和 MQ 都是以服务为范围进行分布式事务的处理,而 XA、BED、SAGA 则是以数据库为范围进行分布式处理。

对于数据库中间来来说,更趋向于选择后者,对于业务而言侵入小,改造的成本低。

对比图

图中暂时未包括:1)本地消息表;2)可靠消息最终一致性方案 。因为,这个是 Sharding Sphere 官方提供的图,嘻嘻。

MQ如何保证分布式事务的最终一致性

分布式事务:业务相关的多个操作,保证它们同时成功或者同时失败最终一致性:只要保证最终的事务是对齐的就行强一致性:每一个过程的

MQ中要保证事务的最终一致性

  • 生产者要保证100%的消息投递。
  • 消费者保证幂等消费。唯一ID+业务自己实现幂等。

分布式MQ的三种语义:

  • at least once:至少一次
  • at most once:至多一次
  • exactly once:有且仅有一次
    • RocketMQ并不能保证exactly once。商业版本当中提供了exactly once的实现机制
    • Kafka:在最新版本的源码中,提供了exactly once的demo
    • RabbitMQ:erlang天生就成了一种屏障

为什么会有分布式事务?

从本地事务来看,我们可以看为两块,一个是 service 产生多个节点,另一个是 resource 产生多个节点。

😈 可能会有胖说,我们就是一个单体应用,不存在这样的情况。OK ,没问题,那么我们回过头来想想用户下单完成,我们需要给用户发短信。如果发送短信失败,可能是网络抖动的原因,我们是不应该去回滚本地事务,那么此时也可以认为是一个分布式事务。

1)service 多个节点

随着互联网快速发展,微服务,SOA等服务架构模式正在被大规模的使用,举个简单的例子,一个公司之内,用户的资产可能分为好多个部分,比如余额,积分,优惠券等等。在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护。

这样的话就无法保证积分扣减了之后,优惠券能否扣减成功。

2)resource 多个节点

同样的,互联网发展得太快了,我们的Mysql一般来说装千万级的数据就得进行分库分表,对于一个支付宝的转账业务来说,你给的朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,所以我们依然无法保证他们能同时成功。

😈 可能会有胖友说,我们数据没做分库分表,不存在这样的情况。OK,没问题,那么我们回过头来想想最常见的场景,系统里引入了 Redis 做缓存,那么 DB 和 Redis 的一致性问题,就是一种分布式事务的场景。

🦅 是否真的要分布式事务?

在说分布式事务的方案之前,首先你一定要明确你是否真的需要分布式事务?

上面说过出现分布式事务的两个原因,其中有个原因是因为微服务过多。我见过太多团队一个人维护几个微服务,太多团队过度设计,搞得所有人疲劳不堪,而微服务过多就会引出分布式事务,这个时候我不会建议你去采用分布式事务的方案,而是请把需要事务的微服务聚合成一个单机服务,使用数据库的本地事务。因为不论任何一种方案都会增加你系统的复杂度,这样的成本实在是太高了,千万不要因为追求某些设计,而引入不必要的成本和复杂度。

当然,如果你是个人的练习 Demo 项目,请使劲的造,拼命的玩。甚至说,我建议你能读完所使用的分布式事务的方案的原理和源码。因为,一旦上了生产,出了问题,你很有可能无从下手~

所以,想清楚你是否需要分布式事务,你是否能够 hold 住分布式事务的解决方案。

什么是分布式事务?

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

或者,在换一句话说,分布式事务 = n 个本地事务。通过事务管理器,达到 n 个本地事务要么全部成功,要么全部失败。

你们公司是如何处理分布式事务的?

如果你真的被问到,可以这么说:

我们某某特别严格的场景,用的是 TCC 来保证强一致性。

你找一个严格资金要求绝对不能错的场景,你可以说你是用的 TCC 方案。


然后其他的一些场景,基于阿里的 RocketMQ 来实现了分布式事务。

如果是一般的分布式事务场景,订单插入之后要调用库存服务更新库存,库存数据没有资金那么的敏感,可以用可靠消息最终一致性方案。

分布式事务分类:柔性事务和刚性事务

分布式场景下,多个服务同时对服务一个流程,比如电商下单场景,需要支付服务进行支付、库存服务扣减库存、订单服务进行订单生成、物流服务更新物流信息等。如果某一个服务执行失败,或者网络不通引起的请求丢失,那么整个系统可能出现数据不一致的原因。

上述场景就是分布式一致性问题,追根到底,分布式一致性的根本原因在于数据的分布式操作,引起的本地事务无法保障数据的原子性引起。

分布式一致性问题的解决思路有两种,一种是分布式事务,一种是尽量通过业务流程避免分布式事务。分布式事务是直接解决问题,而业务规避其实通过解决出问题的地方(解决提问题的人)。其实在真实业务场景中,如果业务规避不是很麻烦的前提,最优雅的解决方案就是业务规避。

分布式事务分类

分布式事务实现方案从类型上去分刚性事务、柔型事务:

  • 刚性事务满足CAP的CP理论
  • 柔性事务满足BASE理论(基本可用,最终一致)

刚性事务

刚性事务:通常无业务改造,强一致性,原生支持回滚/隔离性,低并发,适合短事务。

原则:刚性事务满足足CAP的CP理论

刚性事务指的是,要使分布式事务,达到像本地式事务一样,具备数据强一致性,从CAP来看,就是说,要达到CP状态。

刚性事务:XA 协议(2PC、JTA、JTS)、3PC,但由于同步阻塞,处理效率低,不适合大型网站分布式场景。

柔性事务

柔性事务指的是,不要求强一致性,而是要求最终一致性,允许有中间状态,也就是Base理论,换句话说,就是AP状态。

与刚性事务相比,柔性事务的特点为:有业务改造,最终一致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务。

柔性事务分为:

  • 补偿型
  • 异步确保型
  • 最大努力通知型。

柔型事务:TCC/FMT、Saga(状态机模式、Aop模式)、本地事务消息、消息事务(半消息)

分布式事务如何处理怎么保证事务一致性?

强一致性:每一笔交易都保证事务一致

最终一致性

误区:认为 分布式事务 = Seata

分布式事务:将不同节点上的事务操作,提供操作原子性保证。同时成功或者同时失败。第一个要点:就是要在原本没有直接关联的事务之间建立联系1、Http连接:尽最大努力通知。– 事后补偿2、MQ:事务消息机制。3、Redis:opKey-> 事务A 开始时 opKey+1,事务B 开始时 opKey +1,事务A结束时 opKey - 1,事务B也一样,最终opKey=0,则事务成功。4、Seata:是通过Transaction来在多个事务之间建立联系。两阶段:AT XA 核心在于要锁资源三阶段:TCC 在两阶段的基础上增加一个准备阶段,在准备阶段是不锁资源的。SAGA模式:类似于熔断。业务自己实现正向操作和补偿操作的逻辑。

分布式事务的基础

数据库的 ACID 满足了数据库本地事务的基础,但是它无法满足分布式事务,这个时候衍生了 CAP 和 BASE 两个经典理论。

🦅 CAP 理论

CAP 定理,又被叫作布鲁尔定理。对于设计分布式系统来说(不仅仅是分布式事务)的架构师来说,CAP 就是你的入门理论。

  • C (一致性):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  • A (可用性):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
  • P (分区容错性):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。

高可用、数据一致性是很多系统设计的目标,但是分区又是不可避免的事情。我们来看一看分别拥有 CA、CP 和 AP 的情况。

  • CA without P:如果不要求 P(不允许分区),则 C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此 CA 的系统更多的是允许分区后各子系统依然保持 CA 。
  • CP without A:如果不要求 A(可用),相当于每个请求都需要在 Server 之间强一致,而 P(分区)会导致同步时间无限延长,如此 CP 也是可以保证的。很多传统的数据库分布式事务都属于这种模式。
  • AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。

可能胖友看完之后,会一脸懵逼,可以看看 《分布式系统理论(一):CAP 定理》 文章提供的示例:

  • MySQL 主从异步复制是 AP 系统。
  • MySQL 主从半同步复制是 CP 系统。
  • Zookeeper 是 CP 系统。
  • Redis 主从同步是 AP 系统。
  • Eureka 主从同步是 AP 系统。

从上的示例中,“三选二”是一个伪命题。不是为了 P(分区容忍性),要在 A 和 C 之间选择一个。分区很少出现,CAP 在大多数时候允许完美的 C 和 A 。但当分区存在或可感知其影响的情况下,就要预备一种策略去探知分区并显式处理其影响。

艿艿,如果关于“三选二”是一个伪命题无法理解,可以回过头在看一眼“CA without P” ,对比下就好理解了。对于单节点,CA 必然是可以保证的。

另外,关于 CAP 的论证过程,也是蛮有趣的一块内容,感兴趣的胖友,可以自己去搜索下。

🦅 BASE 理论

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性) 三个短语的缩写。是对 CAP 中AP 的一个扩展

  • BA 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
  • S 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致。
  • E 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE 解决了 CAP 中理论没有网络延迟,在 BASE 中用软状态和最终一致,保证了延迟后的一致性。

BASE 和 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

对于大部分的分布式应用而言,只要数据在规定的时间内达到最终一致性即可。我们可以把符合传统的 ACID 叫做刚性事务,把满足 BASE 理论的最终一致性事务叫做柔性事务。

一味的追求强一致性,并非最佳方案。对于分布式应用来说,刚柔并济是更加合理的设计方案,即在本地服务中采用强一致事务,在跨系统调用中采用最终一致性。如何权衡系统的性能与一致性,是十分考验架构师与开发者的设计功力的。

具体到分布式事务的实现上,业界主要采用了 XA 协议的强一致规范以及柔性事务的最终一致规范。

艿艿:所以,市面上的分布式事务的解决方案,除了 XA 协议是强一直的,其他都是最终一致的。

分布式事务的实现主要有哪些方案?

分布式事务的实现主要有以下 6 种方案:

  • XA 方案
  • TCC 方案
  • 本地消息表
  • 可靠消息最终一致性方案
  • 最大努力通知方案
  • SAGA

聊聊 Saga 方案

Saga 是 30 年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

Saga 的组成如下:

  • 每个 Saga 由一系列 sub-transaction Ti 组成
  • 每个Ti 都有对应的补偿动作 Ci ,补偿动作用于撤销 Ti 造成的结果。这里的每个 T ,都是一个本地事务。
  • 可以看到,和 TCC 相比,Saga 没有“预留 try”动作 ,它的 Ti 就是直接提交到库。

Saga的执行顺序有两种:

  • 子事务序列 T1, T2, …, Tn得以完成 (最佳情况)。
  • 或者序列 T1, T2, …, Tj, Cj, …, C2, C1, 0 < j < n, 得以完成。

Saga 定义了两种恢复策略:

向后恢复:补偿所有已完成的事务,如果任一子事务失败。

向后恢复,即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction ,这种做法的效果是撤销掉之前所有成功的 sub-transation ,使得整个 Saga 的执行结果撤销。

向前恢复:重试失败的事务,假设每个子事务最终都会成功。

显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机、网络可能会失败,甚至数据中心也可能会停电,这时需要提供故障恢复后回退的机制,比如人工干预。

🦅 如何解决没有 Prepare阶段可能带来的问题?

由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性,当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发。例如:

  • 在应用层面加锁。
  • 应用层面预先冻结资源。

还是拿 100 元买一瓶水的例子来说。

  • 这里定义:
    • T1=扣100元 T2=给用户加一瓶水 T3=减库存一瓶水
    • C1=加100元 C2=给用户减一瓶水 C3=给库存加一瓶水
  • 我们一次进行 T1,T2,T3。如果发生问题,就执行发生问题的 C 操作的反向。上面说到的隔离性的问题会出现在,如果执行到 T3 这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题。

艿艿:也就是说,给的太早,但是可以被取消!

可以看见 Saga 模式没有隔离性的影响还是较大,可以参照华为的解决方案:

  • 从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。
  • 也可以在业务层面通过预先冻结资金的方式隔离这部分资源,最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。

🦅 解决方案

  • Apache Service Comb 的 Saga 事务引擎
  • Sharding Sphere 的 Saga 支持

实际是基于 Apache Service Comb 的 Saga 事务引擎之上进行开发。

聊聊 TCC 方案

TCC 模型是把锁的粒度完全交给业务处理,它需要每个子事务业务都实现Try-Confirm / Cancel 接口。

TCC 模式本质也是 2PC ,只是 TCC 在应用层控制。

  • Try :
    • 尝试执行业务
    • 完成所有业务检查(一致性)
    • 预留必须业务资源(准隔离性)
  • Confirm :
    • 确认执行业务;
    • 真正执行业务,不作任何业务检查
    • 只使用Try阶段预留的业务资源
    • Confirm 操作满足幂等性
  • Cancel :
    • 取消执行业务
    • 释放Try阶段预留的业务资源
    • Cancel操作满足幂等性

这三个阶段,都会按本地事务的方式执行。不同于 XA的prepare ,TCC 无需将 XA 的投票期间的所有资源挂起,因此极大的提高了吞吐量。

🦅 应用场景

下面对TCC模式下,A账户往B账户汇款100元为例子,对业务的改造进行详细的分析:

汇款服务和收款服务分别需要实现,Try-Confirm-Cancel 接口,并在业务初始化阶段将其注入到 TCC 事务管理器中。

汇款服务

  • Try
    • 检查A账户有效性,即查看A账户的状态是否为“转帐中”或者“冻结”
    • 检查A账户余额是否充足
    • 从A账户中扣减 100 元,并将状态置为“转账中”
    • 预留扣减资源,将从 A 往 B 账户转账 100 元这个事件存入消息或者日志中
  • Confirm
    • 不做任何操作
  • Cancel
    • A 账户增加 100 元
    • 从日志或者消息中,释放扣减资源

收款服务

  • Try
    • 检查 B 账户账户是否有效;
  • Confirm
    • 读取日志或者消息,B 账户增加 100 元
    • 从日志或者消息中,释放扣减资源;
  • Cancel
    • 不做任何操作

由此可以看出,TCC 模型对业务的侵入强,改造的难度大。

但是,在需要前置资源锁定的场景,不得不使用 XA 或 TCC 的方式。再例如说,下单场景,在订单创建之前,需要扣除如下几个资源:

  • 优惠劵
  • 钱包余额
  • 积分
  • ….

那么,不得不进行前置多资源锁定,无非是使用 XA 的强锁,还是 TCC 的弱锁。在 oceans 的 tag 0.0.1 中,在未使用 TCC 的情况下,模拟 TCC 的效果的苦闷。

当然,如果能不用 TCC 的情况下,尽量不要用 TCC 。因为,编写回滚逻辑的代码,可能会比较恶心。

🦅 解决方案?

聊聊 XA 方案

XA 是 X/Open CAE Specification (Distributed Transaction Processing)模型,它定义的 TM(Transaction Manager)与 RM(Resource Manager)之间进行通信的接口。

Java中 的 javax.transaction.xa.XAResource 定义了 XA 接口,它依赖数据库厂商对 jdbc-driver 的具体实现。

  • mysql-connector-java-5.1.30 的实现可参 com.mysql.jdbc.jdbc2.optional.MysqlXAConnection 类。

在 XA 规范中,数据库充当 RM 角色,应用需要充当 TM 的角色,即生成全局的 txId ,调用 XAResource 接口,把多个本地事务协调为全局统一的分布式事务。

目前 XA 有两种实现:

  • 基于一阶段提交( 1PC ) 的弱 XA 。
  • 基于二阶段提交( 2PC ) 的强 XA 。

弱 XA

弱 XA 的顺序图

  • 弱 XA 通过去掉 XA 的 Prepare 阶段,以达到减少资源锁定范围而提升并发性能的效果。典型的实现为在一个业务线程中,遍历所有的数据库连接,依次做 commit 或者 rollback 。
  • 弱 XA 同本地事务相比,性能损耗低,但在事务提交的执行过程中,若出现网络故障、数据库宕机等预期之外的异常,将会造成数据不一致,且无法进行回滚。

🦅 解决方案?

基于弱 XA 的事务无需额外的实现成本,相对容易。目前支持的有:

强 XA

强 XA 的顺序图

  • 二阶段提交是 XA 的标准实现。它将分布式事务的提交拆分为 2 个阶段:prepare 和 commit/rollback 。
    • 第一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
    • 第二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。
  • 开启 XA 全局事务后,所有子事务会按照本地默认的隔离级别锁定资源,并记录 undo 和 redo 日志。然后由 TM 发起 prepare 投票,询问所有的子事务是否可以进行提交:
    • 当所有子事务反馈的结果为 “yes” 时,TM 再发起 commit 。
    • 若其中任何一个子事务反馈的结果为“no”,TM 则发起 rollback 。
    • 如果在 prepare 阶段的反馈结果为 “yes” ,而 commit 的过程中出现宕机等异常时,则在节点服务重启后,可根据 XA recover 再次进行 commit 补偿,以保证数据的一致性。

🦅 优点?

尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。

🦅 缺点?

单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。

艿艿:如果事务管理器是 Proxy 模式的数据库中间件,并且实现高可用,可能可以解决这个问题。不太肯定,需要到时翻下 Sharding Sphere 的源码。TODO

同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。

数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。艿艿:此处的数据不一致也问题不大,因为使用 xa 会锁定记录,无法被访问。

🦅 解决方案?

Sharding Sphere

Sharding Sphere 支持基于 XA 的强一致性事务解决方案,可以通过 SPI 注入不同的第三方组件作为事务管理器实现 XA 协议,如 Atomikos 和 Narayana 。

Spring JTA + Atomikos

应用场景

这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。

这个方案,我们很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。我可以给大家介绍一下, 现在微服务,一个大的系统分成几百个服务,几十个服务。一般来说,我们的规定和规范,是要求每个服务只能操作自己对应的一个数据库

如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,可能会出现数据被别人改错,自己的库被别人写挂等情况。

如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。

distributed-transacion-XA

聊聊可靠消息最终一致性方案

这个的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。

大概的意思就是:

distributed-transaction-reliable-message

  • A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
  • 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;
  • 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
  • mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
  • 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。

这个还是比较合适的,目前国内互联网公司大都是这么玩儿的

🦅 解决方案

RocketMQ 事务消息,源码解析,可见 《RocketMQ 源码分析 —— 事务消息》

虽然 RocketMQ 早期开源事务消息后又阉割闭源,但是在 RocketMQ 4.3 版本中,又重新提供。所以,不要搞错落。

《RabbitMQ 之消息确认机制(事务+Confirm)》

Kafka 事务消息,https://zhuanlan.zhihu.com/p/42046847

聊聊最大努力通知方案

艿艿瞅了瞅市面上的资料,分别有两种解释。或者说,两种不同的解决方案。

解释一

最大努力送达,是针对于弱 XA 的一种补偿策略。它采用事务表记录所有的事务操作 SQL 。

  • 如果子事务提交成功,将会删除事务日志。
  • 如果执行失败,则会按照配置的重试次数,尝试再次提交,即最大努力的进行提交,尽量保证数据的一致性,这里可以根据不同的业务场景,平衡 C 和 A ,采用同步重试或异步重试。

🦅 优点

无锁定资源时间,性能损耗小。

🦅 缺点

尝试多次提交失败后,无法回滚,它仅适用于事务最终一定能够成功的业务场景。.🦅 总结

因此 BED 是通过事务回滚功能上的妥协,来换取性能的提升。

貌似,暂时也想象不到具体的使用场景。

🦅 解决方案

正如上图,提供的解决方式 Sharding-JDBC ,具体的源码解析,可见 《Sharding-JDBC 源码分析 —— 分布式事务(一)之最大努力型》

解释二

这个方案的大致意思就是:

  • 系统 A 本地事务执行完之后,发送个消息到 MQ;
  • 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
  • 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。

🦅 解决方案

按照这个解释,RocketMQ 的消息重试,符合这个解释。具体的源码解析,见 《RocketMQ 源码分析 —— 定时消息与消息重试》

比较常见的场景,就是支付成功后,多次回调~

聊聊本地消息表

本地消息表,其实是 国外的 Ebay 搞出来的这么一套思想 。

这个大概意思是这样的:

distributed-transaction-local-message-table

  1. A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;
  2. 接着 A 系统将这个消息发送到 MQ 中去;
  3. B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
  4. B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;
  5. 如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
  6. 这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那边成功为止。

这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的,会导致如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用。

本地消息队列是 BASE 理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。

本文由作者按照 CC BY 4.0 进行授权