目录
一、分布式事务问题
1、本地事务
本地事务,也就是传统的单机事务,在传统数据库事务中,必须要满足四个原则:
2、分布式事务
分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务,例如:
- 跨数据源的分布式事务
- 跨服务的分布式事务
- 综合情况
在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例,包括下面几个行为:
- 创建新订单
- 扣减商品库存
- 从用户账户余额扣除金额
订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。但是当我们把三件事情看做一个"业务",要满足保证"业务"的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。此时ACID难以满足,这是分布式事务要解决的问题。
3、分布式事务问题
就好比说我现在要创建一个订单,呢我就要去调用账户服务去获取金额,再调用库存服务去扣减商品库存等等,如果有一个服务中的操作失败了,呢么其余的还是会进行,会导致库存减少了,但是没付钱的这种情况等等,这就不符合一个正常业务的功能和逻辑。
二、理论基础
解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。
1、CAP定理
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标:
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
(1)一致性
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。比如现在包含两个节点,其中的初始数据是一致的:
当我们修改其中一个节点的数据时,两者的数据产生了差异:
要想保住一致性,就必须实现node01 到 node02的数据同步:
(2)可用性
Availability(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝如图,有三个节点的集群,访问任何一个都可以及时得到响应:
当有部分节点因为网络故障或其它原因无法访问时,代表节点不可用:
(3)分区容错
Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务。
(4)矛盾
在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此Partition Tolerance不可避免。
当节点接收到新的数据变更时,就会出现问题了:
如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性,就不能等待网络恢复,那node01、node02与node03之间就会出现数据不一致。
也就是说,在P一定会出现的情况下,A和C之间只能实现一个。
2、BASE理论
BASE理论是对CAP的一种解决思路,包含三个思想:
- Basically Available(基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
3、解决分布式事务的思路
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:
- AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
- CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
但不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC):
这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务。
三、初识Seata
Seata是 2019 年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。
1、Seata的架构
Seata事务管理中有三个重要的角色:
- TC(Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
整体的架构图如图:
Seata基于上述架构提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入。
- TCC模式:最终一致的分阶段事务模式,有业务侵入。
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式。
- SAGA模式:长事务模式,有业务侵入。
无论哪种方案,都离不开TC,也就是事务的协调者。
2、部署TC服务
首先我们要下载seata-server包,地址在http://seata.io/zh-cn/blog/download.html。
然后修改配置,conf目录下的registry.conf文件:
registry {
# tc服务的注册中⼼类,这⾥选择nacos,也可以是eureka、zookeeper等
type = "nacos"
nacos {
# seata tc 服务注册到 nacos的服务名称,可以⾃定义
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# 读取tc服务端的配置⽂件的⽅式,这⾥是从nacos配置中⼼读取,这样如果tc是集群,可以共
享配置
type = "nacos"
# 配置nacos地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
服务端配置文件seataServer.properties文件需要在nacos中配:
主要配置代码如下:
# 数据存储⽅式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatc
hedStatements=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、⽇志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客户端与服务端传输⽅式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提⾼性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
然后创建数据库,最后启动TC服务:进入bin目录,运行其中的seata-server.bat即可默认端⼝为8091,或者运行指令指定端口:seata-server.bat -p 9000 -m file。启动成功后,seata-server应该已经注册到nacos注册中心了。
3、 微服务集成Seata
(1)引入依赖
</dependency>
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
${seata.version} 是 Maven 中的一个占位符,表示 Seata 依赖的版本号。这通常在项目的 pom.xml 文件中通过 <properties> 标签来定义。
(2)配置TC地址
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-tc-server # seata服务名称
username: nacos
password: nacos
tx-service-group: seata-demo # 事务组名称
service:
vgroup-mapping: # 事务组与cluster的映射关系
seata-demo: SH
这里的type: nacos:指定注册中心的类型为 Nacos。
nacos的server-addr是指定 Nacos 服务器的地址,127.0.0.1:8848 是默认的本地 Nacos 服务地址。
namespace是指定 Nacos 的命名空间,默认为空。命名空间可以用于环境隔离。
group: DEFAULT_GROUP是指定 Nacos 中的服务分组,默认是 DEFAULT_GROUP。
application指定 Seata TC 服务在 Nacos 中的服务名称。
username 和 password:用于连接 Nacos 的认证信息,分别为 nacos。
tx-service-group: seata-demo:定义事务组名称为 seata-demo,用于标识分布式事务的逻辑分组。
vgroup-mapping是事务组和集群之间的映射关系,seata-demo: SH,将 seata-demo 事务组映射到集群 SH。这意味着所有属于 seata-demo 组的事务都会在 SH 集群上执行。
这段配置的作用是将 Seata 事务协调器注册到 Nacos 注册中心,同时定义了事务组与集群的映射关系。微服务在处理分布式事务时,会通过这些配置找到相应的 TC 服务,并根据映射关系来执行事务。
(3)其他服务
其它微服务也都参考订单服务的步骤来做,完全一样。
四、实例代码
Seata中的四种不同的事务模式。
1、XA模式
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA规范提供了支持。
(1)两阶段提交
XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。
正常情况:
异常情况:
一阶段:
- 事务协调者通知每个事物参与者执行本地事务。
- 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁。
二阶段:
事务协调者基于一阶段的报告来判断下一步操作:
- 如果一阶段都成功,则通知所有事务参与者,提交事务。
- 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务。
也就是说它每个事务执行完后先不进行提交,等全部都完成后通过是否全部成功来判断是否提交事务。
(2)Seata的XA模型
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型:
RM一阶段的工作:
- 注册分支事务到TC。
- 执行分支业务sql但不提交。
- 报告执行状态到TC。
TC二阶段的工作:
- TC检测各分支事务执行状态
- 如果都成功,通知所有RM提交事务
- 如果有失败,通知所有RM回滚事务
RM二阶段的工作:接收TC指令,提交或回滚事务。
数据库表结构:
(3)优缺点
XA模式的优点是什么?
- 事务的强一致性,满足ACID原则。
- 常用数据库都支持,实现简单,并且没有代码侵入。
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差。
- 依赖关系型数据库实现事务。
(4)实现XA模式
Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:
修改application.yml文件(每个参与事务的微服务),开启XA模式:
seata:
data-source-proxy-mode: XA
给发起全局事务的入口方法添加@GlobalTransactional注解:本例中是OrderServicelmpl中的create方法。
@Override
@GlobalTransactional
// @Transactional
public Order createOrder(int pid) {
//1.通过fegin调用商品微服务-查询商品信息
Product product = productService.findByPid(pid);
//2.下单(创建订单)
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(pid);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);//设置购买数量
orderMapper.insert(order);
//3.通过fegin调用商品微服务-扣库存
productService.reduceInventory(pid, order.getNumber());
return order;
}
其中@GlobalTransactional是Seata 提供的注解,用于开启全局分布式事务。这个注解确保了方法内部的所有操作要么全部成功,要么全部回滚。
@Transactional是Spring 提供的注解,用于本地事务管理。如果开启,方法内的数据库操作会在同一个事务中进行。但它只能应用于同一个事务,而不能应用于全局事务,多服务事务。
剩下的和前面所写的逻辑均一致,就是进行查询商品和添加订单操作。
最后重启服务并测试:重启order-service,再次测试,发现无论怎样,微服务都能成功回滚。
2、AT模式
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
(1)Seata的AT模型
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:
阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:删除undo-log即可。
阶段二回滚时RM的工作:根据undo-log恢复数据到更新前。
就相当于是我每个模块的事物提交后不用等待其他事物的完成,直接提交或回滚,但是会更新一个undo-log快照,这个undo-log快照保存的是你之前提交事务之前的数据记录,在所有的模块服务都运行完成之后,它进行检查,看是否有提交失败的的情况,如果有呢么就用快照覆盖现在的数据,相当于就是恢复了之前的数据,然后再删除快照。如果没有的话,就直接删除快照即可。
(2)AT与XA的区别
简述AT模式与XA模式最大的区别是什么?
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
- XA模式强一致;AT模式最终一致。
(3)脏写问题
在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:
这个问题主要出现在了,事务1把money改成了90,这是提交了事务,但是没有完全提交,保存的快照用于恢复如果其他服务没成功的话。接着在事务1还没进行阶段二判断的时候,事务2插进来将数据改变,money改成了80,并提交了事务。这时候b看的数据是90,减去10成为了80。但是紧接着事务1又因为其他服务没有成功被快照进行了恢复,money变成了最开始的100,呢么这时候事务2的减10操作就变成了脏写。
解决思路就是引入了全局锁的概念在释放DB锁之前,先拿到全局锁,避免同一时刻有另外一个事务来操作当前数据。
相当于就是在事务1开始的时候进行了引入全局锁,这个锁可以把当前事务封锁,也就是事务1在操作的时候一直等到阶段二判断完回滚或者提交完,事务2才能进行操作,这样就避免了脏写。
(4)优缺点
AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好。
- 利用全局锁实现读写隔离。
- 没有代码侵入,框架自动完成回滚和提交。
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致。
- 框架的快照功能会影响性能,但比XA模式要好很多。
(5)实现AT模式
AT模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。 只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。
导入数据库表,记录全局锁:
运行seata-at.sql,其中lock_table导入到TC服务关联的数据库,undo_log表导入到微服务关联的数据库:
其中的TC数据库上面XA已经展示过了,这里只展示undo_log表(在你的服务表中创建):
修改application.yml文件,将事务模式修改为AT模式即可:
seata:
data-source-proxy-mode: AT # 默认就是AT
其中的操作跟前面写的均一样,最后重启服务并测试。