分布式事务解决方案对比(预研)

1 篇文章 0 订阅
1 篇文章 0 订阅

1.背景

业务的发展,系统架构的不断演进,原来业务处理都是在一个服务系统进行,通过Spring框架中的事物处理机制就可以很好地控制,业务的发展,单个服务系统架构,不能满足发展,现在把单个服务系统拆分为多个服务进行协同完成业务,这样就会出现事务的问题;

如:客户注册时需要处理以下业务,创建客户信息、创建管理员信息、开通CFCA账户、开通银行账户等;银行账户的开通需要适应多银行中选择,进行开通。这样就会出现事务问题,要成功全部成功,要么失败所有的请求都必须回退,需要严格要求数据一致性。

事务的实现是非常重要,使用使用数据库进行事务管理是不能实现,分库分表的情况下是不能实现,因此需要引入分布式事务技术,来解决事务问题。

2.目标

  1、首先是业务需求,为了解耦业务;其次是为了减少业务与业务之间的相互影响。

    2、数据一致性

    3、高可用、高性能

3.什么是分布式事务

要想理解分布式事务,我们首先来看一下什么是事务。

事务,其实是包含一系列操作的、一个有边界的工作序列,有明确的开始和结束标志,且要么被完全执行,要么完全失败,即 all or nothing。通常情况下,我们所说的事务指的都是本地事务,也就是在单机上的事务。

    而分布式事务,就是在分布式系统中运行的事务,由多个本地事务组合而成。在分布式场景下,对事务的处理操作可能来自不同的机器,甚至是来自不同的操作系统。文章开头提到的电商处理订单问题,就是典型的分布式事务。

    要深入理解分布式事务,我们首先需要了解它的特征。分布式事务是多个事务的组合,那么事务的特征 ACID,也是分布式事务的基本特征,其中 ACID 具体含义如下:

         原子性(Atomicity),即事务最终的状态只有两种,全部执行成功和全部不执行。若处理事务的任何一项操作不成功,就会导致整个事务失败。一旦操作失败,所有操作都会被取消(即回滚),使得事务仿佛没有被执行过一样。

         一致性(Consistency),是指事务操作前和操作后,数据的完整性保持一致或满足完整性约束。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元 ; 一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况 (该情况,用户 A 和 B 均为 600 元,总共 1200 元)。

         隔离性(Isolation),是指当系统内有多个事务并发执行时,多个事务不会相互干扰,即一个事务内部的操作及使用的数据,对其他并发事务是隔离的。

         持久性(Durability),也被称为永久性,是指一个事务完成了,那么它对数据库所做的更新就被永久保存下来了。即使发生系统崩溃或宕机等故障,只要数据库能够重新被访问,那么一定能够将其恢复到事务完成时的状态。分布式事务基本能够满足 ACID,其中的 C 是强一致性,也就是所有操作均执行成功,才提交最终结果,以保证数据一致性或完整性。但随着分布式系统规模不断扩大,复杂度急剧上升,达成强一致性所需时间周期较长,限定了复杂业务的处理。为了适应复杂业务,出现了

 

4.分布式事务的5种实现方法

    分布式事务主要是解决在分布式环境下,组合事务的一致性问题。实现分布式事务有以下 5 种基本方法:基于 XA 协议的二阶段提交协议方法;三阶段提交协议方法;基于TCC事务补偿机制;基于消息的最终一致性方法;阿里Seata分布式事务解决方。

4.1、基于XA的二阶段提交

第一阶段

 

第二阶段提交

第二阶段回滚

思想:

    系统中的事务管理器做为协调者,负责各个本地资源的提交和回滚,而资源管理器就是分布事务的参与者,通过投票阶段和提交阶段,协调事务的操作,保持数据的一致性

特点

    强一致性

    同步执行

    算法简单实现

缺点

    同步阻塞问题

    单点故障问题

    数据不一致问题

    性能低

    系统吞吐量低

4.2、三阶段提交

思想:

    有CanCommit、PreCommit、DoCommit三个阶段,引入超时机制和准备机制,解决2PC的同步阻塞和单点故障问题。

特点

    强一致性

    同步执行

    无同步阻塞问题

    无单点故障问题

缺点

    数据不一致问题

    性能低

系统吞吐量不高

 

4.3、基于TCC事务补偿机制

 思想:

TCC 分为三个阶段,即 Try、Confirm、Cancel 三个阶段。

Try 阶段:主要尝试执行业务,执行各个服务中的 Try 方法,主要包括预留操作;

Confirm 阶段:确认 Try 中的各个方法执行成功,然后通过 TM 调用各个服务的 Confirm 方法,这个阶段是提交阶段;

Cancel 阶段:当在 Try 阶段发现其中一个 Try 方法失败,例如预留资源失败、代码异常等,则会触发 TM 调用各个服务的 Cancel 方法,对全局事务进行回滚,取消执行业务。

 特点:

    最终一致性

    检查数据一致,锁定数据

    高性能

缺点:

    业务的侵入性非常大

    数据库需要增加字段表示状态

    业务逻辑的每个分支都需要实现try、confirm、cancel三个操作

    业务要保证每个方法对幂等

    改造成本高

 

4.4、基于分布式消息的最终一致性

思想:

将事务通过消息或者日志的方式来异步执行,消息或者日志可以存本地文件、数据库、或者消息队列中,再通过业务规则进行失败重试或回退。

特点:

    最终一致性

    异步执行

    无同步阻塞问题

    无单点故障问题

    性能高

    系统吞吐量高

缺点:

    算法复杂难道高

    对业务代码耦合非常紧

 

4.5、阿里云Seata分布式事务解决方案(推荐使用)

Seata基础架构

应用中的使用架构图

思想:

    Seata 的基础建模和 DTP 模型类似,只不过前者是将事务管理器分得更细了,抽出一个事务协调器(Transaction Coordinator 简称 TC),主要维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。而 TM 则负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

特点

    对业务无侵入

    高性能

缺点

    需要数据库支持,Mysql 5.6以上版本支持XA协议

    jar 最低版本 SpringBoot  -> 1.5.x.RELEASE

5.Seata 介绍

根据上图可知整个TXC模型有三个重要的组件

         Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚 (单独部署)

         Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议

         Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚

简单理解就是TM事务管理器通过RPC与TC通讯请求开启一个全局事务

简单理解过程就是: Business作为服务起始方(此时它是TM)发起全局事务并注册到TC。在调用协同服务时,协同服务的事务分支事务会先完成阶段一的事务提交或回滚,并生成事务回滚的undo_log日志,同时注册当前服务到TC并上报其事务状态,归并到同一个业务的全局事务中。此时若没有问题继续下一个服务的调用,期间任何服务的分支事务回滚,都会通知到TC,TC在通知全局事务包含的所有已完成一阶段提交的分支事务回滚。如果所有分支事务都正常,最后回到全局事务发起方时,也会通知到TC,TC在通知全局事务包含的所有分支删除回滚日志。在这个过程中为了解决写隔离和读隔离的问题会涉及到TC管理的全局锁。

下面是一个分布式事务在seata中的执行流程:

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。

  2. XID 在微服务调用链路的上下文中传播。

  3. RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。

  4. TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。

  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

5.1 Seata Server 介绍

整个Server模块可以分成7个主要模块

         RPC模块 负责与TM RM交互

         Coordinator Core模块 TC实现事务协调的核心模块

         Lock模块 资源全局锁的实现

         Config模块 支持配置TC的配置模块

         Store模块 TC运行时全局事务以及分支事务的相关信息需要通过Store模块持久化

         Discover模块 Seata TC服务注册发现模块

         HA-Cluste模块 TC Server实现高可用的模块

5.2 Seata支持的模式

5.2.1 AT模式

执行阶段:

调用业务定义的 Try 方法(完全由业务层面保证 可回滚 和 持久化)

完成阶段:

分支提交:调用各事务分支定义的 Confirm 方法

分支回滚:调用各事务分支定义的 Cancel 方法

5.2.2 TCC模式

执行阶段:

调用业务定义的 Try 方法(完全由业务层面保证 可回滚 和 持久化)

完成阶段:

分支提交:调用各事务分支定义的 Confirm 方法

分支回滚:调用各事务分支定义的 Cancel 方法

5.2.3 XA模式

执行阶段:

可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证可回滚

持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)

完成阶段:

分支提交:执行 XA 分支的 commit

分支回滚:执行 XA 分支的 rollback

XA模式的优势

  1.    满足全局数据一致性,因为 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离。

  2.    业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。

  3.    数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。

  4.    多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少,为不同语言开发 SDK 较之 AT 模式将更 薄,更容易。

  5.    传统基于 XA 应用的迁移:传统的,基于 XA 协议的应用,迁移到 Seata 平台,使用 XA 模式将更平滑。

5.3保证事务的隔离性

因seata一阶段本地事务已提交,为防止其他事务脏读脏写需要加强隔离。

脏读:select语句加for update,代理方法增加@GlobalLock或@GlobalTransaction

 

脏写:必须使用@GlobalTransaction

注:如果你查询的业务的接口没有GlobalTransactional 包裹,也就是这个方法上压根没有分布式事务的需求,这时你可以在方法上标注@GlobalLock 注解,并且在查询语句上加 for update。 如果你查询的接口在事务链路上外层有GlobalTransactional注解,那么你查询的语句只要加for update就行。设计这个注解的原因是在没有这个注解之前,需要查询分布式事务读已提交的数据,但业务本身不需要分布式事务。 若使用GlobalTransactional注解就会增加一些没用的额外的rpc开销比如begin 返回xid,提交事务等。GlobalLock简化了rpc过程,使其做到更高的性能。

5.4 配置文件说明

registry.conf

该配置用于指定 TC 的注册中心和配置文件,默认都是 file; 如果使用其他的注册中心,要求 Seata-Server 也注册到该配置中心上

registry.conf

file.conf

该配置用于指定TC的相关属性;如果使用注册中心也可以将配置添加到配置中心

file.conf

启动 Seata-Server

在 https://github.com/seata/seata/releases 下载相应版本的 Seata-Server,修改 registry.conf为相应的配置(如果使用 file 则不需要修改),解压并通过以下命令启动:sh ./bin/seata-server.sh

使用@GlobalTransactional开启事务

在业务的发起方的方法上使用@GlobalTransactional开启全局事务,Seata 会将事务的 xid 通过拦截器添加到调用其他服务的请求中,实现分布式事务

 

 

 

 

6.方案具体实施

综合Seata 模式的介绍并根据当前业务上并发要求不是很高,选择使用XA模式。

 

 

6.2部署文档

6.2.1业务服务对接

步骤一:升级spring boot 版本

 

<parent>

   <groupId>org.springframework.boot</groupId>

   <artifactId>spring-boot-starter-parent</artifactId>

   <version>1.5.22.RELEASE</version>

</parent>

 

步骤二: 引入依赖包

 

<dependency>

    <groupId>io.seata</groupId>

    <artifactId>seata-spring-boot-starter</artifactId>

    <version>1.2.0</version>

</dependency>

 

步骤三:业务服务添加配置

在业务服务的application.properties添加配置

 

#seata事务组名

seata.tx-service-group=my_test_tx_group

#事务组名my_test_tx_group使用的seata服务名称,在seata服务配置registry.eureka.application上配置

seata.service.vgroup-mapping.my_test_tx_group=seata-server

#序列化方式,默认的jackson在使用springboot1.5.22.RELEASE有版本问题

seata.client.undo.log-serialization=fastjson

#使用的注册中心方式

seata.registry.type=eureka

seata.registry.eureka.weight=1

#eureka地址

seata.registry.eureka.service-url=http://localhost:9001/eureka

#不使用自动数据源代理,使用了XA模式会有问题

seata.enable-auto-data-source-proxy=false

 

步骤四:配置数据源代理

 

@Bean

@ConfigurationProperties(prefix = "spring.datasource")

public DruidDataSource druidDataSource() {

    return new DruidDataSource();

}

 

@Bean("dataSourceProxy")

public DataSource dataSource(DruidDataSource druidDataSource) {

    // DataSourceProxy for AT mode

    // return new DataSourceProxy(druidDataSource);

 

    // DataSourceProxyXA for XA mode

    return new DataSourceProxyXA(druidDataSource);

}

 

步骤五:在业务上添加分布式事务注解@GlobalTransactional

只需要在事务最初开始的入口服务对于的方法加上注解,下层服务不需要加任何代码(需要maven依赖seata相关包和seata客户端相关配置)。

例如:

 

 

/**

* 减库存,下订单

*

* @param userId

* @param commodityCode

* @param orderCount

*/

@GlobalTransactional

public void purchase(String userId, String commodityCode, int orderCount) {

    LOGGER.info("purchase begin ... xid: " + RootContext.getXID());

    storageClient.deduct(commodityCode, orderCount);

    orderClient.create(userId, commodityCode, orderCount);

}

6.2.2 Seata Server 部署

步骤一:下载seata server包(开发提供)或通过https://github.com/seata/seata/releases 进行下载

步骤二:修改配置registry.conf和file.conf

registry.conf:修改注册类型和eureka地址,如下

 

registry {

  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa

 

#1.改成eureka

  type = "eureka"

  eureka {

 

#2.改成线上的eureka地址,集群逗号

    serviceUrl = "http://localhost:8761/eureka"

    application = "default"

    weight = "1"

  }

 

 

File.conf: 修改存储方式,如下:

 

store {

  ## store mode: file、db

 

#1.改成db

  mode = "db"

  ## database store property

  db {

    datasource = "druid"

    ## mysql/oracle/postgresql/h2/oceanbase etc.

    dbType = "mysql"

    driverClassName = "com.mysql.jdbc.Driver"

 

#2.修改数据库连接地址和用户名密码

    url = "jdbc:mysql://127.0.0.1:3306/db_seata_server"

    user = "root"

    password = "root"

    minConn = 5

    maxConn = 30

    globalTable = "global_table"

    branchTable = "branch_table"

    lockTable = "lock_table"

    queryLimit = 100

    maxWait = 5000

  }

 

步骤三:命令启动:sh ./bin/seata-server.sh

步骤四:将部署好的第一个节点复制到另外一台机器再启动就可以

6.3资源评估

服务名称

节点数量

网络要求

内存

备注

Seata server

2

内网通信

2G*2

 

注册中心(eureka)

--

内网通信

--

使用微服务的注册中心

 

6.4性能和异常情况测试

测试场景:

1.批量操作:批量删除、批量更新、批量新增

2.运行时,某业务服务突然宕机

 

3.某业务服务长耗时执行逻辑。

模拟订单服务耗时处理的压测结果

 

 

6.5风险评估

1.  升级spring boot风险点

Spring boot1.5.4.RELEASE 升级到 Spring boot1.5.22.RELEASE小版本升级,影响当前业务服务范围相对比较小。

Seata1.2.0使用了jackson作为undolog的序列化工具,jackson-databind版本为2.9.7与spring boot1.5.22.RELEASE版本依赖的有冲突,解决方案是将序列号工具改成fastjson,当前已经测试通过。

2.  不支持复合主键

暂不支持,建议先建一列自增id主键,原复合主键改为唯一键来规避下

3.  seata server服务宕机

2.1已经在运行的业务服务会报异常

io.seata.common.exception.FrameworkException: can not connect to services-server

2.2后面启动的服务可以起来,但是也会报连接 seata server异常。

2.3开启@GlobalTransactional(分布式事务声明)的逻辑无法正常使用,报异常

can not register RM,err:can not connect to services-server.

2.4未开启@GlobalTransactional(分布式事务声明)的逻辑能够正常使用,

4.  死锁后的操作

3.1事务嵌套导致的死锁

当项目中存在seata事务和spring 事务嵌套时,seata事务会出现死锁的问题,需要人工将seata_server数据库表branch_table和global_table相关数据删除。

5.  添加异常告警机制

添加企业微信机器人告警。告警规范使用webhook方式。

 

附录

Seata相关概念

Resource Manager (RM):

控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

Transaction Manager (TM)

控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

Transaction Coordinator (TC)

事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

XID:全局事务生成一个全局唯一的 XID,在微服务调用链路的上下文中传播,将分支事务串联起来。

 

XA规范

1. X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。

2. 描述了全局的事务管理器与局部的资源管理器之间的接口。

3. 的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。

4.使用两阶段提交(2PC,Two-Phase Commit)来保证所有资源同时提交或回滚任何特定的事务。

5. 在上世纪 90 年代初就被提出。目前,几乎所有主流的数据库都对 XA 规范 提供了支持。

 

 

 

MySQL事务隔离级别

事务隔离级别

脏读

不可重复读

幻读

读未提交(read-uncommitted)

不可重复读(read-committed)

可重复读(repeatable-read)

串行化(serializable)

 

事务的并发问题

1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

参考资料 

 https://blog.csdn.net/l1028386804/article/details/79769043

https://mp.weixin.qq.com/s/uYF7bE9Ob-0hfFVNO7Pcpw

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值