SpringCloud基础6——分布式事务,Seata

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 分布式事务、ACID原则、CAP定理、Seata、Seata的四种分布式方案:XA、AT、TCC、SAGA模式

 导航:

【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析

目录

1.分布式事务问题

1.1.本地事务

1.1.1.MySQL事务的ACID原则

1.1.2.MySQL事务的隔离级别

1.1.3.事务的传播行为

1.1.4.本地事务代理对象、事务传播行为不生效问题

1.2.分布式事务

1.3.演示分布式事务问题

2.理论基础

2.1.CAP定理

2.1.1.一致性C:数据同步

2.1.2.可用性A:节点正常访问

2.1.3.分区容错P:分区后也要对外提供服务

2.1.4.矛盾

2.2.BASE理论

2.3.解决分布式事务的思路,最终一致和强一致

3.初识Seata

3.1.Seata的架构,TC,TM,RM

3.2.部署Seata的TC(事务协调者)服务,tc-server

1.下载

2.解压

3.registry.conf修改Seata的注册中心、配置中心

4.在nacos添加配置

5.创建数据库表,分支事务表和全局事务表

6.启动TC服务

3.3.微服务集成Seata

3.3.1.引入依赖

3.3.2.配置TC地址

3.3.3.其它服务

4.详解Seata的四种分布式事务方案

4.1.XA模式,两阶段都加锁

4.1.1.两阶段提交

4.1.2.Seata的XA模型

4.1.3.优缺点

4.1.4.实现XA模式

4.2.AT模式:一阶段直接提交,二阶段统一回滚

4.2.1.Seata的AT模型

4.2.2.流程梳理

4.2.3.AT与XA的区别

4.2.4.脏写问题,全局锁

4.2.5.优缺点

4.2.6.实现AT模式(完整流程)

4.2.7.@Transactional和@GlobalTransactional对比

4.3.TCC模式:基于资源预留

4.3.1.流程分析

4.3.2.Seata的TCC模型

4.3.3.优缺点

4.3.4.事务悬挂和空回滚

4.3.5.下单代码实现TCC模式,@TwoPhaseBusinessAction,@BusinessActionContextParameter,

4.4.SAGA模式:基于一系列本地事务

4.4.1.原理

4.4.2.优缺点

4.5.四种模式对比,XA,AT,TCC,SAGA

5.其他分布式事务方案(不使用Seata)

5.1.柔性事务方法实现最终一致性

5.1.1.柔性事务-最大努力通知型方案

5.1.2.柔性事务-可靠消息+最终一致性方案(异步确保型)

5.2.xxl-job和消息组件实现最终一致性

6.高可用

6.1.高可用架构模型

6.2.实现高可用,异地容灾

6.2.1.模拟异地容灾的TC集群

6.2.2.将事务组映射配置到nacos

3.微服务读取nacos配置


1.分布式事务问题

1.1.本地事务

1.1.1.MySQL事务的ACID原则

只要是事务,就必须要满足四个原则:

ACID是指事务四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

原子性:事务的所有操作,要么全部成功,要么全部失败。

一致性:事务前后,数据库的约束没有被破坏,保持前后一致。

隔离性:操作同一资源的并发事务之间相互隔离,不会互相干扰。

持久性:事务的结果最终一定会持久化到数据库,宕机等故障也无法影响。

image.gif

1.1.2.MySQL事务的隔离级别

事务隔离级别是指操作同一资源的并发事务之间的隔离度。隔离级别越高,事务之间的相互干扰就越小,安全性就越高。

读问题:

  • 脏读:读到了脏数据。当前事务读到另一个未提交事务刚改的数据。只有读未提交会脏读。
  • 不可重复读:前后重复读的数据不一样。前后两次读同数据,这期间数据被其他事务改了,导致前后读取的数据不同。
  • 幻读:前后读的数据是一样,但多了几行或少了几行,像幻觉一样。事务前后读的数据集合不同,导致出现“幻像”行。仅串行化能解决幻读问题。

事务隔离级别

  • 读未提交:事务能读到所有未提交事务的数据。压根不加锁、没隔离,性能最高。
  • 读提交:事务能读到已提交事务的数据。底层由MVVC实现 。解决脏读问题。
  • 可重复读(默认):前后读的数据相同。底层由MVVC实现 。解决脏读、不可重复读问题。
  • 串行化:事务一拿到锁阻塞其他事务,直到释放锁。读时共享锁,写时排它锁。阻塞导致性能最差。解决脏读、不可重复读、幻读问题。

MVVC:多版本并发控制。

共享锁:在共享锁下,多个线程可以同时读取数据,但只有一个线程能够修改数据。当一个线程在修改数据时,必须获得独占锁,以便其他线程不能访问数据。

排它锁:在排它锁下,只有一个线程可以修改数据,其他线程不允许访问数据。

SQL 标准定义了四种隔离级别,这四种隔离级别分别是:

- 读未提交(READ UNCOMMITTED)

- 读提交 (READ COMMITTED);

- 可重复读 (REPEATABLE READ);

- 串行化 (SERIALIZABLE)。

4种隔离级别MySQL都支持,并且InnoDB存储引擎默认的支持隔离级别是可重复读REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock的锁算法,因此避免了幻读的产生。所以,InnoDB存储引擎在默认的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别;

下面是四种隔离级别在解决脏读、不可重复读、幻读问题方面的情况:

隔离级别 脏读 不可重复读 幻读
读未提交 存在 存在 存在
读已提交 不存在 存在 存在
可重复读 不存在 不存在 存在
串行化 不存在 不存在 不存在

脏读(Dirty Read):指一个事务读取了另一个未提交的事务所写入的数据,如果隔离级别越高,则越不容易出现脏读问题。

不可重复读(Non-Repeatable Read):指一个事务在读取同一数据时,由于另外一个事务的修改或删除,导致两次读取的数据不同。如果隔离级别越高,则越不容易出现不可重复读问题。

幻读(Phantom Read):指一个事务多次执行同一个查询,但每次返回的数据集合都不同,导致出现“幻像”行。如果隔离级别越高,则越不容易出现幻读问题。

1.1.3.事务的传播行为

PROPAGATION_REQUIRED(默认): 如果当前没有事务, 就创建一个新事务, 如果当前存在事务,就加入该事务, 该设置是最常用的设置。

image.gif

PROPAGATION_SUPPORTS: 支持当前事务, 如果当前存在事务, 就加入该事务, 如果当前不存在事务, 就以非事务执行。

PROPAGATION_MANDATORY: 支持当前事务, 如果当前存在事务, 就加入该事务, 如果当前不存在事务, 就抛出异常。

PROPAGATION_REQUIRES_NEW: 创建新事务, 无论当前存不存在事务, 都创建新事务。

PROPAGATION_NOT_SUPPORTED: 以非事务方式执行操作, 如果当前存在事务, 就把当前事务挂起。

PROPAGATION_NEVER: 以非事务方式执行, 如果当前存在事务, 则抛出异常。

PROPAGATION_NESTED: 如果当前存在事务, 则在嵌套事务内执行。 如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。

1.1.4.本地事务代理对象、事务传播行为不生效问题

理论上,下面代码,执行a方法,因为事务失败,b会回滚、c不会回滚;a,b共用一个事务,a的超时时间会覆盖b的超时时间。

但是,因为方法a、b、c都在同一个service里面,事务传播行为不生效,他们之间只是一个简单的方法调用,共享一个事务

原理:事务是用代理对象来控制的,内部调用b(),c(),就相当于直接调用没有经过事务【绕过了代理对象】

//1、如果方法a、b、c都在同一个service里面,事务传播行为不生效,共享一个事务
//  原理:事务是用代理对象来控制的,内部调用b(),c(),就相当于直接调用没有经过事务【绕过了代理对象】
//  解决:不能使用this.b();也不能注入自己【要使用代理对象来调用事务方法】
    
    
@Transactional(timeout=30)
public void a() {
  b();// a事务传播给了b事务,并且b事务的设置失效
  c();// c单独创建一个新事务
}
@Transactional(propagation = Propagation.REQUIRED, timeout=2)
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {
}

image.gif

解决:要使用代理对象来调用事务方法。不能使用this.b();也不能注入自己

1.引入aop的starter依赖,里面有aspectj依赖

<!-- 引入aop,解决本地事务失效问题 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency

image.gif

2、开启动态代理【默认使用jdk动态代理,需要有接口】

@EnableAspectJAutoProxy(exposeProxy = true)  //开启了aspect动态代理模式,对外暴露代理对象

image.gif

3、获取动态代理对象

OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy(); 
orderService.b(); 
orderService.c();

image.gif

1.2.分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

分布式事务的场景:

跨JVM进程:微服务架构下,远程调用

image.gif

跨数据库:单服务操作多个数据库:

注意这里说的是多个数据库,而不是同一个数据库下的多个表

image.gif

多服务单数据库:

image.gif

分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务,例如:

  • 跨数据源的分布式事务
  • 跨服务的分布式事务
  • 综合情况

在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例,包括下面几个行为:

  • 创建新订单
  • 扣减商品库存
  • 从用户账户余额扣除金额

完成上面的操作需要访问三个不同的微服务和三个不同的数据库。

image.gif

订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。

但是当我们把三件事情看做一个"业务",要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。

此时ACID难以满足,这是分布式事务要解决的问题

1.3.演示分布式事务问题

我们通过一个案例来演示分布式事务的问题:

1)创建数据库,名为seata_demo,然后导入课前资料提供的SQL文件:

image.gif

订单表order_tbl

image.gif

账户表account_tbl

image.gif

库存表storage_tbl:

image.gif

2)导入课前资料提供的微服务:

image.gif

微服务结构如下:

image.gif

记得改一下yml中数据库密码

其中:

seata-demo:父工程,负责管理项目依赖

  • account-service:账户服务,负责管理用户的资金账户。提供扣减余额的接口
  • storage-service:库存服务,负责管理商品库存。提供扣减库存的接口
  • order-service:订单服务,负责管理订单。创建订单时,需要调用account-service和storage-service

3)启动nacos、所有微服务

startup.cmd -m standalone

image.gif

4)测试下单功能,发出Post请求:

请求如下:

坑点:

feign报错解决:添加feign的httpclient连接池;    在orderservice添加spring.cloud.loadbalancer.retry.enabled=true

  • 报错几次后,每次检查数据库,账户金额别小于200,库存数量别少于2.因为这里微服务还没有实现分布式事务问题,下单、扣钱、减库存,哪一环节出错其他环节的数据都不会回滚。

测试正确操作:

http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=2&money=200

image.gif

发现订单新增成功、钱减少了200,数量少了2。

image.gif

测试报错操作、分布式事务不会滚问题(减库存后库存是负数,报错、回滚):

http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=20&money=200

image.gif

如图:

image.gif

测试发现,当库存不足时,如果余额已经扣减,并不会回滚,出现了分布式事务问题。

image.gif

2.理论基础

解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。

2.1.CAP定理

1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标,三个指标不可能同时做到

  • Consistency(一致性)
  • Availability(可用性)
  • Partition tolerance (分区容错性)

image.gif

它们的第一个字母分别是 C、A、P。

Eric Brewer 说,这三个指标不可能同时做到,最多只能同时满足两个。这个结论就叫做 CAP 定理。

2.1.1.一致性C:数据同步

Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。

比如现在包含两个节点,其中的初始数据是一致的:

image.gif

当我们修改其中一个节点的数据时,两者的数据产生了差异:

image.gif

要想保住一致性,就必须实现node01 到 node02的数据 同步:

image.gif

2.1.2.可用性A:节点正常访问

Availability (可用性):用户访问集群中的任意健康节点必须能得到响应而不是超时或拒绝

如图,有三个节点的集群,访问任何一个都可以及时得到响应:

image.gif

当有部分节点因为网络故障或其它原因无法访问时,代表节点不可用:

image.gif

2.1.3.分区容错P:分区后也要对外提供服务

Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接形成独立分区。

image.gif

Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

2.1.4.矛盾

在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此Partition Tolerance不可避免。

当节点接收到新的数据变更时,就会出现问题了:

image.gif

如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。

如果此时要保证可用性,就不能等待网络恢复,那node01、node02与node03之间就会出现数据不一致。

也就是说,在P一定会出现的情况下,A和C之间只能实现一个。要么让node1、node2停掉,使三个节点保持同步,要么让node1、node2组成新分区,对外提供服务。

2.2.BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:

  • Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
  • Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
  • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后最终达到数据一致。

2.3.解决分布式事务的思路,最终一致和强一致

分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:

  • AP模式(最终一致):各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致
  • CP模式(强一致):各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态

但不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC)

image.gif

这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务

3.初识Seata

Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。

官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。

image.gif

3.1.Seata的架构,TC,TM,RM

Seata事务管理中有三个重要的角色:

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

整体的架构如图:

image.gif

Seata基于上述架构提供了四种不同的分布式事务解决方案:

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
  • TCC模式:最终一致的分阶段事务模式,有业务侵入
  • AT模式(默认):最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
  • SAGA模式:长事务模式,有业务侵入

无论哪种方案,都离不开TC,也就是事务的协调者。

3.2.部署Seata的TC(事务协调者)服务,tc-server

1.下载

首先我们要下载seata-server包,地址在http😕/seata.io/zh-cn/blog/download.html

当然,课前资料也准备好了:

image.gif

2.解压

在非中文目录解压缩这个zip包,其目录结构如下:

image.gif

3.registry.conf修改Seata的注册中心、配置中心

修改conf目录下的registry.conf文件:

image.gif

内容如下:

registry {
  ## tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
  type = "nacos"    #注册中心类型是nacos
                    
  nacos {
    ## seata tc 服务注册到 nacos的服务名称,可以自定义
    application = "seata-tc-server"
    serverAddr = "127.0.0.1:8848"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "SH"
    username = "nacos"
    pass改成自己的密码word = "nacos"
  }
}
config {
  ## 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
  type = "nacos"    #也可以设置成file类型配置,会从本目录下找"file.conf"读取配置
  ## 配置nacos地址等信息
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    pass改成自己的密码word = "nacos"
    dataId = "seataServer.properties"
  }
}

image.gif

4.在nacos添加配置

特别注意,为了让tc服务的集群可以共享配置,我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好。

格式如下:

image.gif

配置内容如下(要修改数据库信息):

#store事务日志存在哪里,这里设成数据库。也可以设成file类型
# 数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123
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

image.gif

其中的数据库地址、用户名、密码都需要修改成你自己的数据库信息。

也可以在文件中配置,需要registry.conf设置配置方式为file:

image.gif

file.conf

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #vgroup->rgroup
  vgroup_mapping.gulimall-order-fescar-service-group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}
client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
}
## transaction log store
store {
  ## store mode: file、db
  mode = "file"
  ## file store
  file {
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }
  ## database store
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "mysql"
    passw改成自己密码ord = "mysql"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"
  local {
    ## store locks in user's database
  }
  remote {
    ## store locks in the seata's server
  }
}
recovery {
  committing-retry-delay = 30
  asyn-committing-retry-delay = 30
  rollbacking-retry-delay = 30
  timeout-retry-delay = 30
}
transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
}
## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}
image.gif

5.创建数据库表,分支事务表和全局事务表

特别注意:tc服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。

新建一个名为seata的数据库,运行课前资料提供的sql文件:

image.gif

这些表主要记录全局事务、分支事务、全局锁信息:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事务表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table`  (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` tinyint(4) NULL DEFAULT NULL,
  `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime(6) NULL DEFAULT NULL,
  `gmt_modified` datetime(6) NULL DEFAULT NULL,
  PRIMARY KEY (`branch_id`) USING BTREE,
  INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- 全局事务表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `timeout` int(11) NULL DEFAULT NULL,
  `begin_time` bigint(20) NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`xid`) USING BTREE,
  INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
  INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;

image.gif

6.启动TC服务

进入bin目录,运行其中的seata-server.bat即可:

image.gif

启动成功后,seata-server应该已经注册到nacos注册中心了。

打开浏览器,访问nacos地址:http://localhost:8848,然后进入服务列表页面,可以看到seata-tc-server的信息:

image.gif

3.3.微服务集成Seata

我们以order-service为例来演示。

3.3.1.引入依赖

首先,在order-service中引入依赖:

<!--seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <!--版本较低,1.3.0,因此排除--> 
        <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>
    <!--seata starter 采用1.4.2版本-->
    <version>${seata.version}</version>
</dependency>

image.gif

3.3.2.配置TC地址

在order-service中的application.yml中,配置TC服务信息,通过注册中心nacos,结合服务名称获取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

image.gif

微服务如何根据这些配置寻找TC的地址呢?

我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:

  • namespace:命名空间
  • group:分组
  • application:服务名
  • cluster:集群名

以上四个信息,在刚才的yaml文件中都能找到:

image.gif

namespace为空,就是默认的public

结合起来,TC服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。

3.3.3.其它服务

其它两个微服务也都参考order-service的步骤来做,完全一样。

4.详解Seata的四种分布式事务方案

下面我们就一起学习下Seata中的四种不同的事务模式。

4.1.XA模式,两阶段都加锁

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。

4.1.1.两阶段提交

XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。

正常情况:

image.gif

异常情况:

image.gif

一阶段:

  • 事务协调者通知每个事务参与者执行本地事务
  • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

二阶段:

  • 事务协调者基于一阶段的报告来判断下一步操作
  • 如果一阶段都成功,则通知所有事务参与者,提交事务
  • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

4.1.2.Seata的XA模型

Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:

image.gif

资源管理者RM一阶段的工作:

① 注册分支事务到TC

② 执行分支业务sql但不提交

③ 报告执行状态到TC

业务协调者TC二阶段的工作:

  • TC检测各分支事务执行状态
    a.如果都成功,通知所有RM提交事务
    b.如果有失败,通知所有RM回滚事务

RM二阶段的工作:

  • 接收TC指令,提交或回滚事务

4.1.3.优缺点

XA模式的优点是什么?

  • 事务的强一致性,满足ACID原则。
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

4.1.4.实现XA模式

Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:

1)修改application.yml文件(每个参与事务的微服务),开启XA模式:

seata:
  data-source-proxy-mode: XA    #开启数据源代理的XA模式。代理数据源,拦截业务sql操作

image.gif

2)给发起全局事务的入口方法添加@GlobalTransactional注解:

本例中是OrderServiceImpl中的create方法,把@Transactional改成@GlobalTransactional

image.gif

3)重启服务并测试

重启order-service,再次测试,发现无论怎样出错(例如操作完成后钱或库存是负数),三个微服务都能成功回滚。

4.2.AT模式:一阶段直接提交,二阶段统一回滚

AT模式同样是分阶段提交的事务模型,不过弥补了XA模型中资源锁定周期过长的缺陷

4.2.1.Seata的AT模型

基本流程图:

image.gif

阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

  • 根据undo-log恢复数据到更新前

4.2.2.流程梳理

我们用一个真实的业务来梳理下AT模式的原理。

比如,现在又一个数据库表,记录用户余额:

id money
1 100

其中一个分支业务要执行的SQL为:

update tb_account set money = money - 10 where id = 1

image.gif

AT模式下,当前分支事务执行流程如下:

一阶段:

1)TM发起并注册全局事务到TC

2)TM调用分支事务

3)分支事务准备执行业务SQL

4)RM拦截业务SQL,根据where条件查询原始数据,形成快照。

{
    "id": 1, "money": 100
}

image.gif

5)RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90

6)RM报告本地事务状态给TC

二阶段:

1)TM通知TC事务结束

2)TC检查分支事务状态

a)如果都成功,则立即删除快照

b)如果有分支事务失败,需要回滚。读取快照数据({"id": 1, "money": 100}),将快照恢复到数据库。此时数据库再次恢复为100

流程图:

image.gif

4.2.3.AT与XA的区别

简述AT模式与XA模式最大的区别是什么?

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚AT模式利用数据快照实现数据回滚。
  • XA模式强一致AT模式最终一致

4.2.4.脏写问题,全局锁

快照恢复数据时数据库被更新。

在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,归根结底是业务之间的隔离问题,导致a业务根据快照恢复数据时,另一个a业务已提交业务成功,导致恢复的数据是未更新时的样子,如图:

image.gif

解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。

image.gif

4.2.5.优缺点

AT模式的优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能比较好
  • 利用全局锁实现读写隔离
  • 没有代码侵入,框架自动完成回滚和提交

AT模式的缺点:

  • 两阶段之间属于软状态,属于最终一致
  • 框架的快照功能会影响性能,但比XA模式要好很多

4.2.6.实现AT模式(完整流程)

AT模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。

只不过,AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。

0)环境准备

安装启动事务协调器seata-server,registry.conf设置注册中心(nacos)和配置中心(file)、各服务导入依赖

<!--seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

image.gif

1)导入数据库表,记录全局锁

每个需要用的seata的数据库导入undo_log表,undo_log表是回滚日志表:

-- 注意此处0.7.0+ 增加字段 context
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

image.gif

2)修改application.yml文件,将事务模式修改为AT模式即可:

seata:
  data-source-proxy-mode: AT # 默认就是AT

image.gif

3)给发起全局事务的入口方法添加@GlobalTransactional注解:

给分布式大事务注解@GlobalTransactional; 每一个远程的小事务用 @Transactional

@GlobalTransactional
@Transactional  // 本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚。
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
  //.....
}

image.gif

4) 配置类注入seata代理数据源

Seata通过代理数据源实现分支事务,如果没有注入,会导致无法回滚问题。

如果使用的是Hikari数据源配置类:

@Configuration
public class MySeataConfig {
    @Autowired
    DataSourceProperties dataSourceProperties;
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties) {
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

image.gif

Druid数据源

@Configuration
public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }
    @Primary
    @Bean
    public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}
image.gif

5)将file.confregistry.conf两个配置文件移动到项目resources下:

前面事务协调器seata-server的配置文件registry.conf设置了配置中心方式为file。

地址:

https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata/account-service/src/main/resources

file.conf

registry.conf

注意:file.conf 的 service.vgroup_mapping 配置必须和spring.application.name一致

org.springframework.cloud:spring-cloud-starter-alibaba-seataorg.springframework.cloud.alibaba.seata.GlobalTransactionAutoConfiguration 类中,默认会使用 ${spring.application.name}-fescar-service-group作为服务名注册到 Seata Server上,如果和file.conf 中的配置不一致,会提示 no available server to connect错误

也可以通过配置 spring.cloud.alibaba.seata.tx-service-group修改后缀,但是必须和file.conf中的配置保持一致

image.gif 或者yml配置service.vgroup_mapping 的服务名:

image.gif

7)启动并测试

4.2.7.@Transactional和@GlobalTransactional对比

结论:给分布式大事务注解@GlobalTransactional; 每一个远程的小事务用 @Transactional

       ①、只用@Transactional

               开启本地事务,对本地事务进行支持。如果用了@Transactional则保证a、b、c操作都在同一个本地事务中执行,并且更新时会加行锁,如果本地事务不统一提交,其他SQL不能再更新此条数据。如果不加@Transactional则默认没有事务a、b、c操作分别执行,不会加行锁,其他SQL都可以随便更新。

           ②、只用@GlobalTransactional

               开启全局事务,保证分布式事务。如果只用@GlobalTransactional,那就保证分布式事务,各个分支事务(本地事务)的统一,但是不能保证各个分支事务(本地事务)操作的统一。各个本地操作在无事务的状态下执行操作,不会加锁,别的操作可以随意修改。

           ③、@GlobalTransactional和@Transactional一起用

               @Transactional保证本地事务一致性,@GlobalTransactional保证全局事务的一致性。

 

4.3.TCC模式:基于资源预留

XA和AT模式都要加锁,性能低。 而TCC模式不需要加锁,因为每次业务会预留资源,例如冻结应付金额方法。不加锁性能高。

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。

需要实现三个方法:

  • Try:阶段1,资源的检测和预留(例如冻结应付金额)
  • Confirm:提交,完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel回滚,预留资源释放,可以理解为try的反向操作。

4.3.1.流程分析

举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。

  • 阶段一( Try )检查余额是否充足,如果充足则冻结金额从0变30,可用余额从100到70

初识余额:

image.gif

余额充足,可以冻结:

image.gif

此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。

  • 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30

确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:

image.gif

此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元

  • 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30

需要回滚,那么就要释放冻结金额,恢复可用金额:

image.gif

4.3.2.Seata的TCC模型

Seata中的TCC模型依然延续之前的事务架构,如图:

image.gif

4.3.3.优缺点

TCC模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点是什么?

  • 代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 做好幂等处理:需要考虑Confirm和Cancel的失败情况,做好幂等处理(例如cancel超时,系统以为cancel失败就又调用了一次cancel,而其实第一次cancel是成功的。多次cancel结果是一样的,就是幂等了。)

代码侵入:

当你的代码引入了一个组件,导致其它代码或者设计,要做相应的更改以适应新组件.这样的情况我们就认为这个新组件具有侵入性

同时,这里又涉及到一个设计方面的概念,就是耦合性的问题.

我们代码设计的思路是"高内聚,低耦合",为了实现这个思路,就必须降低代码的侵入性.

4.3.4.事务悬挂和空回滚

1)空回滚

某分支事务因为阻塞导致try阶段没有执行,超时后TC通知全局事务回滚,而该分支因为try压根就没有执行,所以无需回滚。

当执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

如图:

image.gif

2)业务悬挂

对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try冻结资源,然而二阶段早已执行完毕,此分支就永远停留在预留资源而不能提交或回滚的状态。

解决办法:数据库记录事务状态。Redis或数据库记录事务状态,每次try前先从Redis或数据库判断是否已经cancel过,每次cancel后修改事务状态为“已cancel”。

事务表:

CREATE TABLE `account_freeze_tbl` (
  `xid` varchar(128) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
  `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
  `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

image.gif

这个表一方面冻结金额、一方面记录状态。

判断空回滚:cancel时根据事务表的id查到状态state为null,就空回滚;

防止业务悬挂:try时查到状态state为2即回滚,就阻止业务悬挂。

4.3.5.下单代码实现TCC模式,@TwoPhaseBusinessAction,@BusinessActionContextParameter,

解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel?

1)思路分析

这里我们定义一张表:

CREATE TABLE `account_freeze_tbl` (
  `xid` varchar(128) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
  `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
  `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

image.gif

其中:

  • xid:是全局事务id
  • freeze_money:用来记录用户冻结金额
  • state:用来记录事务状态

业务实现方法:

  • Try业务:
  • 记录冻结金额和事务状态到account_freeze表
  • 扣减account表可用金额
  • Confirm业务
  • 根据xid删除account_freeze表的冻结记录
  • Cancel业务
  • 修改account_freeze表,冻结金额为0,state为2
  • 修改account表,恢复可用金额
  • 如何判断是否空回滚?
  • cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
  • 如何避免业务悬挂?
  • try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务

接下来,我们改造account-service,利用TCC实现余额扣减功能。

2)声明TCC接口

TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明

我们在account-service项目中的cn.itcast.account.service包中新建一个接口,声明TCC三个接口:

package cn.itcast.account.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
//声明为ttc
@LocalTCC
public interface AccountTCCService {
    //在try方法上@TwoPhaseBusinessAction声明try、confirm、cancel的方法名
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    //参数注解@BusinessActionContextParameter将参数放在上下文对象,在本接口所有方法都能拿到这个参数
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money")int money);
    //confirm方法名要跟@TwoPhaseBusinessAction声明的一致,BusinessActionContext上下文对象获取try方法注解@BusinessActionContextParameter的参数
    boolean confirm(BusinessActionContext ctx);
    boolean cancel(BusinessActionContext ctx);
}

image.gif

3)编写实现类

在account-service服务中的cn.itcast.account.service.impl包下新建一个类,实现TCC业务:

package cn.itcast.account.service.impl;
import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;
    @Override
    //业务注解
    @Transactional
    public void deduct(String userId, int money) {
        // 0.获取事务id,RootContext是seata提供的工具类,用来获取id。
        String xid = RootContext.getXID();
        
        //判断事务悬挂,如果有冻结记录则是悬挂,直接return。对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。
        if(accountMapper.selectById(xid)==null) return;
        // 1.扣减可用余额,这里不用余额判断,因为数据库余额字段类型是非负,扣成负数就业务失败
        accountMapper.deduct(userId, money);
        // 2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        //自定义常量AccountFreeze.State.TRY值就是0
        freeze.setState(AccountFreeze.State.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
    }
    @Override
    public boolean confirm(BusinessActionContext ctx) {
        // 1.获取事务id,也可以用RootContext获取xid
        String xid = ctx.getXid();
        // 2.根据id删除冻结记录,confirm方法不用幂等处理,因为它是直接删除,删多少次都是删
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }
    @Override
    public boolean cancel(BusinessActionContext ctx) {
        // 0.查询冻结记录
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);
        
        //判断空回滚。当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。
        if(freeze==null){freeze =new freeze;设置userId,state,xid;freezeMapper.insert(freeze);return true}
        //幂等判断,如果已经处理过一次cancel时不需要再cancel了(可能源于超时被认为失败其实没失败,再cancel了一次)。confirm方法不用幂等处理,因为它是直接删除,删多少次都是删
        if(freeze.getState()==AccountFreeze.State.CANCEL){return true;}
        // 1.恢复可用余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 2.将冻结金额清零,状态改为CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);
        return count == 1;
    }
}

image.gif

测试:

先修改金额为不够和库存数量为充足、再次发请求:

http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=1&money=2000000

image.gif

发现报错、回滚。

4.4.SAGA模式:基于一系列本地事务

Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。

其理论基础是Hector & Kenneth 在1987年发表的论文Sagas

Seata官网对于Saga的指南:https://seata.io/zh-cn/docs/user/saga.html

4.4.1.原理

Saga 模式使用一系列本地事务来提供事务管理。 本地事务是由 Saga 参与者执行的原子工作。 每个本地事务都会更新数据库,并发布消息或事件来触发 Saga 中的下一个本地事务。 如果本地事务失败,则 Saga 将执行一系列补偿事务,以撤消上一个本地事务所做的更改。

在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。

没有全局锁,也没有冻结资源,所以存在脏写问题。

脏写:快照恢复数据时数据库被更新。

在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,归根结底是业务之间的隔离问题,导致a业务根据快照恢复数据时,另一个a业务已提交业务成功,导致恢复的数据是未更新时的样子

image.gif

Saga也分为两个阶段:

  • 一阶段:直接提交本地事务
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

4.4.2.优缺点

优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高,适合长事务
  • 一阶段直接提交事务,无锁,性能好
  • 不用编写TCC中的三个阶段,实现简单

缺点:

  • 软状态持续时间不确定,时效性差
  • 没有锁,没有事务隔离,会有脏写

4.5.四种模式对比,XA,AT,TCC,SAGA

我们从以下几个方面来对比四种实现:

  • 一致性:能否保证事务的一致性?强一致还是最终一致?
  • 隔离性:事务之间的隔离性如何?
  • 代码侵入:是否需要对业务代码改造?
  • 性能:有无性能损耗?
  • 场景:常见的业务场景

如图:

image.gif

XA:一阶段锁定数据库资源,业务协调者TC通知各业务执行并获取执行状态,二阶段如果存在业务失败,则通知所有业务回滚。

AT:一阶段资源管理者RM记录undo-log(数据快照),二阶段如果存在业务失败,根据快照回滚。全局锁是在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。

TCC:一阶段try方法资源的检测和预留,二阶段如果存在业务失败,根据Cancel方法编写的内容释放预留资源

SAGA:一阶段直接提交本地事务,二阶段失败则通过编写补偿业务来回滚

5.其他分布式事务方案(不使用Seata)

5.1.柔性事务方法实现最终一致性

5.1.1.柔性事务-最大努力通知型方案

按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对

这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合 MQ 进行实现,例如: 通过 MQ 发送 http 请求,设置最大通知次数。达到通知次数后即不再通知。

案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调。

5.1.2.柔性事务-可靠消息+最终一致性方案(异步确保型)

业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。

业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送

案例:

在商品下单业务的最后要锁定库存,我们设置在锁定库存后发RabbitMQ延迟队列消息,通知锁定库存成功,两分钟后消费消息,根据库存信息查询检查订单是否存在,若不存在代表下订单失败,此时要回滚,也就是解锁库存。

5.2.xxl-job和消息组件实现最终一致性

在主要业务的数据库里创建消息表和消息记录表;

在执行主要业务成功后插入一条消息表数据,xxl-job定时扫描任务列表并发处理任务。因为主要业务的表和消息表在一个数据库里,所以是本地事务,不是分布式事务。

image.gif

举例:

例如课程发布业务, 在课程信息入库后,插入一条消息表数据,课程id为业务信息1,阶段1是存redis、阶段2是ES添加索引,阶段3是静态化页面到文件系统:

强一致:

如果要满足CP就表示课程发布操作后向数据库、redis、elasticsearch、MinIO写四份数据,只要有一份写失败其它的全部回滚。

最终一致:

课程发布操作后,先更新数据库中的课程发布状态,更新后向redis、elasticsearch、MinIO写课程信息,只要在一定时间内最终向redis、elasticsearch、MinIO写数据成功即可。

 

6.高可用

Seata的TC服务作为分布式事务核心,一定要保证集群的高可用性。

6.1.高可用架构模型

搭建TC服务集群非常简单,启动多个TC服务,注册到nacos即可。

但集群并不能确保100%安全,万一集群所在机房故障怎么办?所以如果要求较高,一般都会做异地多机房容灾。

比如一个TC集群在上海,另一个TC集群在杭州:

image.gif

微服务基于事务组(tx-service-group)与TC集群的映射关系,来查找当前应该使用哪个TC集群。当SH集群故障时,只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。

6.2.实现高可用,异地容灾

6.2.1.模拟异地容灾的TC集群

计划启动两台seata的tc服务节点:

节点名称 ip地址 端口号 集群名称
seata 127.0.0.1 8091 SH
seata2 127.0.0.1 8092 HZ

之前我们已经启动了一台seata服务,端口是8091,集群名为SH。

现在,将seata目录复制一份,起名为seata2

修改seata2/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 = "HZ"
    username = "nacos"
    pass改成自己的密码word = "nacos"
  }
}
config {
  ## 读取tc服务端的配置文件的方式,这里是从nacos配置中心读取,这样如果tc是集群,可以共享配置
  type = "nacos"
  ## 配置nacos地址等信息
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    pass改成自己的密码word = "nacos"
    dataId = "seataServer.properties"
  }
}

image.gif

进入seata2/bin目录,然后运行命令:

seata-server.bat -p 8092

image.gif

打开nacos控制台,查看服务列表:

image.gif

点进详情查看:

image.gif

6.2.2.将事务组映射配置到nacos

接下来,我们需要将tx-service-group与cluster的映射关系都配置到nacos配置中心。

新建一个配置:

image.gif

配置的内容如下:

## 事务组映射关系
service.vgroupMapping.seata-demo=SH
service.enableDegrade=false
service.disableGlobalTransaction=false
## 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
## RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
## TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
## undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100

image.gif

3.微服务读取nacos配置

接下来,需要修改每一个微服务的application.yml文件,让微服务读取nacos中的client.properties文件:

seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
      group: SEATA_GROUP
      data-id: client.properties

image.gif

重启微服务,现在微服务到底是连接tc的SH集群,还是tc的HZ集群,都统一由nacos的client.properties来决定了。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
6天前
|
存储 SpringCloudAlibaba Java
【SpringCloud Alibaba系列】一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论
一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论。
【SpringCloud Alibaba系列】一文全面解析Zookeeper安装、常用命令、JavaAPI操作、Watch事件监听、分布式锁、集群搭建、核心理论
|
22天前
|
消息中间件 SQL 中间件
大厂都在用的分布式事务方案,Seata+RocketMQ带你打破10万QPS瓶颈
分布式事务涉及跨多个数据库或服务的操作,确保数据一致性。本地事务通过数据库直接支持ACID特性,而分布式事务则需解决跨服务协调难、高并发压力及性能与一致性权衡等问题。常见的解决方案包括两阶段提交(2PC)、Seata提供的AT和TCC模式、以及基于消息队列的最终一致性方案。这些方法各有优劣,适用于不同业务场景,选择合适的方案需综合考虑业务需求、系统规模和技术团队能力。
151 7
|
1月前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
68 6
|
1月前
|
数据库
如何在Seata框架中配置分布式事务的隔离级别?
总的来说,配置分布式事务的隔离级别是实现分布式事务管理的重要环节之一,需要认真对待和仔细调整,以满足业务的需求和性能要求。你还可以进一步深入研究和实践 Seata 框架的配置和使用,以更好地应对各种分布式事务场景的挑战。
38 6
|
1月前
|
消息中间件 运维 数据库
Seata框架和其他分布式事务框架有什么区别
Seata框架和其他分布式事务框架有什么区别
30 1
|
2月前
|
人工智能 文字识别 Java
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
尼恩,一位拥有20年架构经验的老架构师,通过其深厚的架构功力,成功指导了一位9年经验的网易工程师转型为大模型架构师,薪资逆涨50%,年薪近80W。尼恩的指导不仅帮助这位工程师在一年内成为大模型架构师,还让他管理起了10人团队,产品成功应用于多家大中型企业。尼恩因此决定编写《LLM大模型学习圣经》系列,帮助更多人掌握大模型架构,实现职业跃迁。该系列包括《从0到1吃透Transformer技术底座》、《从0到1精通RAG架构》等,旨在系统化、体系化地讲解大模型技术,助力读者实现“offer直提”。此外,尼恩还分享了多个技术圣经,如《NIO圣经》、《Docker圣经》等,帮助读者深入理解核心技术。
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
|
3月前
|
存储 NoSQL Redis
SpringCloud基础7——Redis分布式缓存,RDB,AOF持久化+主从+哨兵+分片集群
Redis持久化、RDB和AOF方案、Redis主从集群、哨兵、分片集群、散列插槽、自动手动故障转移
SpringCloud基础7——Redis分布式缓存,RDB,AOF持久化+主从+哨兵+分片集群
|
3月前
|
消息中间件 Java 对象存储
数据一致性挑战:Spring Cloud与Netflix OSS下的分布式事务管理
数据一致性挑战:Spring Cloud与Netflix OSS下的分布式事务管理
63 2
|
3月前
|
SpringCloudAlibaba API 开发者
新版-SpringCloud+SpringCloud Alibaba
新版-SpringCloud+SpringCloud Alibaba