前言


  • 学习资料链接

  • 写这篇博客旨在制作笔记,巩固知识。同时方便个人在线阅览,回顾知识。
  • 博客的内容主要来自视频内容和资料中提供的学习笔记。



微服务技术栈导学


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_02


上一篇SpringCloud 微服务技术栈_高级篇①_微服务保护


6.分布式事务问题


6.1.本地事务


本地事务,也就是传统的单机事务

在传统数据库事务中,必须要满足以下四个原则:原子性、一致性、隔离性、持久性


事务的 ACID 原则


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_03


6.2.分布式事务


分布式事务

  • 在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务,
    要保证所有分支事务最终状态一致,这样的事务就是分布式事务
  • 即不是在单个服务或单个数据库架构下产生的事务

例如

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

在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。

例如电商行业中比较常见的下单付款案例,包括下面几个行为:

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

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_04

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

但是当我们把三件事情看做一个 “业务”,要满足保证 “业务” 的原子性

  • 要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是 分布式系统下的事务 了。

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


6.3.演示分布式问题


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


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_05

数据库中有三张表,表结构和表中内容如下

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_06

此处只是演示,所以没有创建三个数据库,但三个表之间也没有外键联系,这里演示用还是行的通的。


  1. 导入课前资料提供的微服务

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_07

导入项目 seata-demo 的项目结构如下

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_08

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

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

  1. 先启动 nacos、在这之后启动所有微服务

~\nacos\bin 目录下启动 nacos 的单机模式

startup.cmd -m standalone

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_09

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_10


  1. 测试下单功能,发出 Post 请求

请求如下

curl --location --request POST 'http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=20&money=200'

这里是借助了 Postman 工具来测试

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_11

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_12


6.4.学习目标


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_13


7.理论基础


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


7.1.CAP 定理


7.1.1.概述


1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_14

Eric Brewer 说,分布式系统无法同时满足这三个指标。这个结论就叫做 CAP 定理。


7.1.2.一致性


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


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_15


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_16


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_17


7.1.3.可用性


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


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_18


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_19


7.1.4.分区容错


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

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


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_20


7.1.5.矛盾


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

因此 Partition Tolerance 不可避免。

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_21

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

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

也就是说,在 P 一定会出现的情况下,A 和 C 之间只能实现一个。


7.2.BASE 理论


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

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

7.3.解决分布式事务的思路


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

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

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


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_22


举例:分布式事务模型图


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


7.4.小结


简述 CAP 定理内容

  • 分布式系统节点通过网络连接,一定会出现分区问题(P)
  • 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足

思考:elasticsearch 集群是 CP 还是 AP?

  • ES 集群出现分区时,故障节点会被剔除集群,数据分片会重新分配到其它节点,保证数据一致。
  • 因此是低可用性,高一致性,属于 CP

简述 BASE 理论三个思想

  • 基本可用、软状态、最终一致

解决分布式事务的思想和模型

  • 全局事务:整个分布式事务
  • 分支事务:分布式事务中包含的每个子系统的事务
  • 最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据
  • 强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚

8.初始 Seta


8.1.简述 Seta


Seata 是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。

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

官网地址:http://seata.io/

该网站的文档、播客中提供了大量的使用说明、源码分析。

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_23


8.2.Seta 架构


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

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_24


整体架构图


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

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

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


8.3.部署 TC 服务


参考课前资料提供的文档 seata的部署和集成.md

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_25


部署 Seata 的 tc-server


8.3.1.下载


首先我们要下载 seata-server 包

下载地址http://seata.io/zh-cn/blog/download.html

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_26


8.3.2.解压查看基本目录


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_27


8.3.3.修改配置文件信息


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_28


内容如下

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"
  }
}

8.3.4.在 Nacos 页面中添加配置


特别注意:为了让 tc 服务的集群可以共享配置,我们选择了 nacos 作为统一配置中心。

因此服务端配置文件 seataServer.properties 文件需要在 nacos 中配好。

GROUP 是 DEFAULT_GROUP

格式如下:

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_29

上面的界面中,需要填写的配置内容如下

# 数据存储方式,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

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


视频中用的是 MySQL5,而我用的是 MySQL8,所以我这里还需要改两个地方

store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=Asia/Shanghai&useUnicode=true&rewriteBatchedStatements=true
store.db.user=username # 你自己设置的用户名
store.db.password=password # 你自己设置的密码

8.3.5.创建数据库表


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

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_30

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

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;

8.3.6.启动 TC 服务


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_31

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

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_32


8.4.微服务集成 Seata


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


8.4.1.引入依赖


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

<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>

<!-- seata starter 采用 1.4.2 版本-->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>${seata.version}</version>
</dependency>

8.4.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

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

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

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

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_33

namespace 为空,就是默认的 public

结合起来,TC 服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH

这样就能确定 TC 服务集群了。然后就可以去 Nacos 拉取对应的实例信息了。


当正在运行的 seata-server.bat 的终端出现如下信息时,就说明微服务集成 Seata 成功

RM register success,\
message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/seata_demo', \
applicationId='account-service', \
transactionServiceGroup='seata-demo'},\
channel:[id: 0xbc148488, \ 
L:/IPd地址:8091 - R:/IP地址:15452],\
client version:1.4.2

8.5.小结


nacos 服务名称组成包括?

  • namespace + group + serviceName + cluster

seata 客户端获取 tc 的 cluster 名称方式?

  • tx-group-service 的值为 key 到 vgroupMapping

9.动手实践


下面学习下 Seata 中的四种不同的事务模式。


9.1.XA 模式


9.1.1.简单介绍


XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准

XA 规范 描述了全局的 TM 与局部的 RM 之间的接口

几乎所有主流的数据库都对 XA 规范 提供了支持。


9.1.2.两阶段提交


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


正常情况


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_34


异常情况


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_35


9.1.3.Seata 的 XA 模型


Seata 对原始的 XA 模式做了简单的封装和改造,以适应自己的事务模型

其基本架构如下图所示

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_36


RM 一阶段的工作

  1. 注册分支事务到 TC
  2. 执行分支业务 sql 但不提交
  3. 报告执行状态到 TC

TC 二阶段的工作

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

RM 二阶段的工作

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

9.1.4.优缺点


XA 模式的优点是什么?

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

XA 模式的缺点是什么?

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

9.1.5.实现 XA 模式


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

  1. 修改 application.yml 文件(每个参与事务的微服务),开启 XA 模式
seata:
  data-source-proxy-mode: XA # 开启数据源代理的 XA 模式
  1. 给发起全局事务的入口方法添加 @GlobalTransactional 注解

本例中是 OrderServiceImpl 中的 create 方法

@Override
//@Transactional
@GlobalTransactional
public Long create(Order order) {
    //省略...... 
}
  1. 重启服务并测试

重启 order-service,再次测试。

在库存不足的情况下,三个微服务都能成功回滚(回滚之后,订单和库存的表的数据都会恢复原样)


9.1.6.Bug 记录


java.lang.NoSuchMethodException: com.mysql.cj.conf.PropertySet.getBooleanReadableProperty(java.lang.String)
	at java.lang.Class.getMethod(Class.java:1786)
	... ...
	
java.lang.NullPointerException
	... ...
	
	... ...
	
java.sql.SQLFeatureNotSupportedException: null	
	... ...

核心在于 NoSuchMethodException,找不到方法,亦即 @GlobalTransactional 注解失效

解决办法是作 MySQL 降级处理(我用的是 8.0.17,更改 maven 配置中的 MySQL 版本为 8.0.11)

<properties>
	<mysql.version>8.0.11</mysql.version>
</properties>

现在再次重启服务,结果发现服务启动不了了…

java.sql.SQLException:
	The connection property 'zeroDateTimeBehavior' acceptable values are: 'CONVERT_TO_NULL', 'EXCEPTION' or 'ROUND'.
	The value 'convertToNull' is not acceptable.

不过通过输出信息就可以得知问题出在 zeroDateTimeBehavior 这里

查看服务中的 application.yml 中关于 MySQL 的配置,发现 zeroDateTimeBehavior=convertToNull

url: jdbc:mysql://localhost:3306/seata_demo?serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true

解决办法就是删除掉 zeroDateTimeBehavior=convertToNull

url: jdbc:mysql://localhost:3306/seata_demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true

然后服务就可以正常启动了,@GlobalTransactional 注解也生效了


9.2.AT 模式


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


9.2.1.Seata 的 AT 模型


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式事务_37


基本流程图


阶段一 RM 的工作

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

阶段二提交时 RM 的工作

  • 删除 undo-log 即可

阶段二回滚时 RM 的工作

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

9.2.2.流程梳理


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


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

id

money

1

100


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

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

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

一阶段

  1. TM 发起并注册全局事务到 TC
  2. TM 调用分支事务
  3. 分支事务准备执行业务 SQL
  4. RM 拦截业务 SQL,根据 where 条件查询原始数据,形成快照。
{
    "id": 1, 
    "money": 100
}
  1. RM 执行业务 SQL,提交本地事务,释放数据库锁。此时 money = 90
  2. RM 报告本地事务状态给 TC

二阶段

  1. TM 通知 TC 事务结束
  2. TC 检查分支事务状态
  • a.如果都成功,则立即删除快照
  • b.如果有分支事务失败,需要回滚。
    读取快照数据({"id": 1, "money": 100}),将快照恢复到数据库。此时数据库再次恢复为 100

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_38


流程图


9.2.3.AT 与 XA 的区别


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

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

9.2.4.脏写问题


在 多线程 并发 访问 AT 模式的分布式事务时,有可能出现脏写问题,如图

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_39


解决思路就是引入 全局锁 的概念。

在释放 DB 锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。


情景1:事务1 和 事务2 都处于 seata 管理下

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_40


情景2:事务1 归 seata 管理,事务2 不归 seata 管理

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_41


9.2.5.优缺点


AT 模式的优点

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

AT 模式的缺点

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

9.2.6.实现 AT 模式


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

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


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

SQL 文件(seata-at.sql)中创建了两张表:lock_table、undo_log

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_42

将 lock_table 表导入到 TC 服务关联的数据库 seata

DROP TABLE IF EXISTS `lock_table`;

CREATE TABLE `lock_table`  (
  `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `branch_id` bigint(20) NOT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`row_key`) USING BTREE,
  INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

将 undo_log 表导入到微服务关联的数据库 seata_demo

DROP TABLE IF EXISTS `undo_log`;

CREATE TABLE `undo_log`  (
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` longblob NOT NULL COMMENT 'rollback info',
  `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
  `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;

  1. 修改 application.yml 文件,将事务模式修改为 AT 模式即可
seata:
  data-source-proxy-mode: AT # 默认就是 AT

  1. 重启服务并测试

9.3.TCC 模式


9.3.1.简述


TCC 模式与 AT 模式非常相似,每阶段都是独立事务。

不同的是 TCC 通过人工编码来实现数据恢复。

需要实现三个方法

  • Try:资源的检测和预留;
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为 try 的反向操作。

9.3.2.流程分析


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


  • 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加 30 元,可用余额扣除 30

初识余额

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_43

余额充足,可以冻结

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_44

此时,总金额 = 冻结金额 + 可用金额,数量依然是 100 不变。

事务直接提交无需等待其它事务


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

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_45

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


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

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_46


9.3.3.Seata 的 TCC 模型


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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_47


9.3.4.优缺点


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

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

TCC 的优点是什么?

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

TCC 的缺点是什么?

  • 有代码侵入,需要人为编写 try、Confirm 和 Cancel 接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑 Confirm 和 Cancel 的失败情况,做好幂等处理

9.3.5.事务悬挂和空回滚


  1. 空回滚

当某分支事务的 try 阶段阻塞时,可能导致全局事务超时而触发二阶段的 cancel 操作。

在未执行 try 操作时先执行了 cancel 操作,这时 cancel 不能做回滚,就是空回滚

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_48

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


  1. 业务悬挂

对于已经空回滚的业务,之前被阻塞的 try 操作恢复,继续执行 try,就永远不可能 confirmcancel,事务一直处于中间状态

这就是业务悬挂

执行 try 操作时,应当判断 cancel 是否已经执行过了,如果已经执行,应当阻止空回滚后的 try 操作,避免悬挂


9.3.6.实现 TCC 模式


案例

  • 改造 account-service 服务,利用 TCC 实现分布式事务

需求

  • 修改 account-service,编写 try、confirm、cancel 逻辑
  • try 业务:添加冻结金额,扣减可用金额
  • confirm 业务:删除冻结金额
  • cancel 业务:删除冻结金额,恢复可用金额
  • 保证 confirm、cancel 接口的幂等性
  • 允许空回滚
  • 拒绝业务悬挂

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

  1. 思路分析
  2. 声明 TCC 接口
  3. 编写实现类
  4. 修改 Controller 类

  1. 思路分析

为了实现空回滚、防止业务悬挂,以及幂等性要求。

我们必须在数据库记录冻结金额的同时,记录当前事务 id 和执行状态。

为此我们设计了一张表

DROP TABLE IF EXISTS `account_freeze_tbl`;

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;

其中:

  • xid:是全局事务 id
  • freeze_money:用来记录用户冻结金额
  • state:用来记录事务状态,0 为 try,1 为 confirm,2 为 cancel

那此时,我们的业务开怎么做呢?

  • 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 业务

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_49

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_50

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


  1. 声明 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;
@LocalTCC
public interface AccountTCCService {	
	/**
	 * Try 逻辑
	 * CTwoPhaseBusinessaction 中的 name 属性要与当前方法名一致
	 * 用于指定 Try 逻辑对应的方法 
	 */
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money")int money);
	
	/** 
	 * 二阶段 confirm 确认方法、可以另命名,但要保证与 commitMethod 一致 
	 *
	 * @param ctx 上下文(context),可以传递 try 方法的参数
	 * @return boolean 执行是否成功
	 */
    boolean confirm(BusinessActionContext ctx);

	/**
	 * 二阶段回滚方法,要保证与 rollbackMethod 一致
	 */
    boolean cancel(BusinessActionContext ctx);
}

  1. 编写实现类

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
        String xid = RootContext.getXID();

        //【悬挂】
        //判断 freeze 中是否有冻结记录,如果有,一定是 cancel 执行过了,我要拒绝业务
        AccountFreeze oldFreeze = freezeMapper.selectById(xid);
        if (oldFreeze != null) {
            return;
        }

        // 1.扣减可用余额
        accountMapper.deduct(userId, money);

        // 2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        // 1.获取事务 id
        String xid = ctx.getXid();

        // 2.根据 id 删除冻结记录
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        // 0.查询冻结记录
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);

        String userId = ctx.getActionContext("userId").toString();

        // 【空回滚】的判断,判断 freeze 是否是 null
        if (freeze == null) {
            //若是 null,则证明 try 没有执行,需要空回滚

            //不过空回滚也不是什么都不做,需要把记录记入 account_freeze 表中
            freeze = new AccountFreeze();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);

            return true;
        }

        // 【幂等判断】
        if (freeze.getState() == AccountFreeze.State.CANCEL) {
            //若为 cancel,则说明已经处理过一次 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;
    }
}

  1. 修改 Controller 类
//@Autowired
//private AccountService accountService;

@Autowired
private AccountTCCService accountService;

9.4.SAGA 模式


9.4.1.简述


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

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

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


9.4.2.原理


在 Saga 模式下

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

分布式事务执行过程中,依次执行各参与者的正向操作

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_分布式_51


Saga 也分为两个阶段

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

9.4.3.优缺点


优点

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

缺点

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

9.5.四种模式对比


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

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

XA

AT

TCC

SAGA

一致性

强一致

弱一致

弱一致

最终一致

隔离性

完全隔离

基于全局锁隔离

基于资源预留隔离

无隔离

代码侵入



有,要编写三个接口

有,要编写状态机和补偿业务

性能



非常好

非常好

场景

对一致性、隔离性有高要求的业务

基于关系型数据库的大多数分布式事务场景都可以

对性能要求较高的事务。


有非关系型数据库要参与的事务。

业务流程长、业务流程多


参与者包含其它公司或遗留系统服务,无法提供 TCC


10.高可用


10.1.高可用架构模型


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

但集群并不能确保 100% 安全,万一集群所在机房故障怎么办?

所以如果要求较高,一般都会做异地多机房容灾。

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_52

微服务基于事务组(tx-service-group)与 TC 集群的映射关系,来查找当前应该使用哪个 TC 集群。

当 SH 集群故障时,只需要将 vgroup-mapping 中的映射关系改成 HZ。

则所有微服务就会切换到 HZ 的 TC 集群了。


10.2.实现高可用


具体实现请参考课前资料提供的文档 seata的部署和集成.md 中的第三章节

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_spring cloud_53


Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_54


10.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"
    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"
  }
}

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

seata-server.bat -p 8092

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

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_55


点进详情查看:

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_学习_56


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


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


新建一个配置

Spring Cloud微服务实战pdf spring微服务实战pdf百度云盘_微服务_57

配置的内容如下

# 事务组映射关系
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

10.2.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

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