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中的执行流程:
-
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
-
XID 在微服务调用链路的上下文中传播。
-
RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
-
TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
-
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模式的优势
-
满足全局数据一致性,因为 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离。
-
业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。
-
数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。
-
多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少,为不同语言开发 SDK 较之 AT 模式将更 薄,更容易。
-
传统基于 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