SpringCloud Alibaba【微服务分布式组件---Seata】

上一篇文章微服务分布式组件—Sentinel

6. Alibaba微服务分布式组件—Seata


事务简介

事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组 SQL 语句组成。事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常成为 ACID 特性

原子性(Atomicity):事务是一个不可分割的工作单位,事务中包含许多操作,要么都做,要么都不做

一致性(Consistency):事务必须使数据库从一个一致性转发太变到另一个一致性状态,事务的中间状态不能被观察到

隔离性(Isolation):一个事务的执行不能被其它事务干扰。即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事物之间不能互相干扰。隔离性又分为四个级别:读未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable read,解决虚读)、 串行执行(serializable,解决幻读)

  • 读未提交:允许另外一个事务读取到当前未提交的数据,隔离级别最低,可能导致脏读、幻读或不可重复读
  • 读已提交:被一个事务修改的数据提交后才能被另外一个事务读取,可以避免脏读,无法避免幻读、而且不可重复读
  • 可重复读:允许重复读,可避免脏读,资源消耗上升。这是MySQL数据库的默认隔离级别
  • 串行执行:也就是按照时间顺序执行多个事务,不存在并发问题,最可靠,但性能与效率最低

持久性(Durability):持久性指一个事务一旦提交,它对数据库中数据的改变应该就是永久性的。接下来的其它操作或故障不应该对其有任何影响

本地事务

@Transactional

大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)本地事务的 ACID 特性是数据库直接提供支持

让我们想象一个传统的单体应用程序。它的业务由3个模块组成。他们使用单一的本地数据源

自然,数据的一致性将由本地事务来保证

在这里插入图片描述

JDBC 编程中,我们通过 java.sql.Connection对象来开启、关闭或者提交事务

微服务架构发生了变化。上面提到的 3 个模块被设计为基于 3 个不同数据源的 3 个服务。每个服务内的数据一致性自然由本地事务保证

在这里插入图片描述

Seata只是解决上述问题的一种方法

在这里插入图片描述

6.1 Seata 是什么

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT 模式是阿里首推的模式,阿里云上有商用版本的 GTS(Global Transaction Service 全局事务服务)

官网:https://seata.io/zh-cn/index.html

源码:https://github.com/seata/seata

官方Demo

Seata 的三大角色

在 Seata 的架构中,一共有三个角色

  • TC(Transaction Coordinator)- 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚

  • TM(Transaction Manager)- 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务

  • RM(Resource Manager)- 资源管理器

管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交和回滚

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端

常见分布式事务解决方案

  • seata 阿里分布式事务框架
  • 消息队列
  • saga
  • XA

他们有一个共同点,都是两阶段(2PC)。“两阶段” 是指完成整个分布式事务,划分成两个步骤完成

2PC 两阶段提交协议(Two-Phase Commit)

分为两个阶段:Prepare 和 Commit

  • Prepare:提交事务请求

在这里插入图片描述

在这里插入图片描述

  1. 询问协调者向所有参与者发送事务请求,询问是否可执行事务操做,然后等待各个参与者的响应
  2. 执行: 各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据库表中的记录),并将 Undo 和 Redo 信息记录事务日志中
  3. 响应: 如果参与者成功执行了十五并写入了 Undo 和 Redo 信息,则向协调者返回 Yes响应,否则返回No响应。当然,参与者也可能宕机,从而不返回响应
  • Commit:执行事务提交

在这里插入图片描述

在这里插入图片描述

  1. commit 请求协调者向所有参与者发送 Commit 请求
  2. 事务提交参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源
  3. 反馈结果参与者执行事务提交后向协调者发送 Ack响应
  4. 完成事务: 接收到所有参与者的 Ack响应后,完成事务提交
  • 中断事务

在执行 Prepare 步骤过程中,如果某些参与者执行事务失败、宕机或与协调者之间的网络中断,那么协调者就无发接收到所有参与者Yes响应。或者某个参与者返回了No响应,此时,协调者就会进入回退流程,对事物进行回滚

在这里插入图片描述

在这里插入图片描述

  1. rollback 请求: 协调者向所有参与者发送 RollBack 请求
  2. 事务回滚: 参与者收到 RollBack 后,使用 Prepare 阶段的 Undo 日志执行事务回滚,完成后释放事务执行其占用所有资源
  3. 反馈结果: 参与者执行事务回滚后相协调这发送Ack响应
  4. 中断事务: 接收到所有参与者的Ack响应后,完成事务中断
2PC 的问题
  1. 同步阴塞 参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去

  2. 单点在2PC中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源

    如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待Prepare响应的时长等),所以也无法顺利处理上一个事务

  3. 数据不一致 Commit 事务过程中 Commit 请求/Rollback请求 可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到 Commit/Rollback请求 ,而其他参与者则正常收到执行了 Commit/Rollback 操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了
    当参与者执行 Commit/Rollback 后会向协调者发送Ack,然而协调者不论是否收到所有的参与者的Ack,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个 “我不确定该事务是否成功”

  4. 环境可靠性依赖 协调者 Prepare 请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC 中,协调者会等待一定时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了

6.2 4种模式的分布式事务实现(AT、TCC、Saga、XA)

AT 模式(Auto)

AT 模式是一种无侵入的分布式事务解决方案

在 AT 模式下,用户只需关注自己的"业务 SQL",用户的"业务 SQL"作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作

在这里插入图片描述

AT 模式如何做到对业务的无侵入

  • 一阶段

在一阶段中,Seata 会拦截 “业务 SQL”,首先解析SQL语义,找到 “业务 SQL” 要更新的业务数据,在业务数据被更新之前,将其保存成before image(undo),然后执行 “业务 SQL” 更新业务数据,在业务数据更新之后,再将其保存成 after image(redo)最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性

在这里插入图片描述

  • 二阶段提交

二阶段如果是提交的话,因为 “业务 SQL” 在一阶段已经提交至数据库,所以 Seata 框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可

在这里插入图片描述

  • 二阶段回滚

二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务 SQL”,还原业务数据。回滚方式便是用 “before image” 还原业务数据;但在还原前首先要校验脏写,对比 “数据库当前业务数据” 和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理

在这里插入图片描述

TCC 模式
  1. 侵入性比较强,并且得自己实现相关事务控制逻辑
  2. 在整个过程基本没有锁得概念,性能也更强

TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方法,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法

在这里插入图片描述

在这里插入图片描述

6.3 Seata—Seata的 AT 模式原理

Seata 托管分布式事务的典型生命周期:

  1. TM(Transaction Manager - 事务管理器)要求 TC(Transaction Coordinator - 事务协调者)开始新的全局事务。TC 生成一个代表全局事务的 XID
  2. XID 通过微服务的调用链传播
  3. RM(Resource Manager - 资源管理器) 将本地事务注册为 XID 对应的全局事务的一个分支到 TC
  4. TM 要求 TC 提交或回滚 XID 对应的全局事务
  5. TC 驱动 XID 对应的全局事务下的所有分支事务完成分支提交或回滚

在这里插入图片描述

整体机制

第一阶段

业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。核心在于对业务 sql 进行解析,转换成 undolog,并同时入库

参考官方文档:https://seata.io/zh-cn/docs/dev/mode/at-mode.html

在这里插入图片描述

第二阶段

分布式事务操作成功,则 TC 通知 RM 异步删除 undolog

在这里插入图片描述

分布式事务操作失败,TM 向 TC 发送回滚请求,RM 收到 协调器 TC 发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚

在这里插入图片描述

整体执行流程

在这里插入图片描述

设计亮点
  • 应用层基于 SQL 解析实现了自动补偿,从而最大程度的降低业务侵入性
  • 将分布式事务中 TC(事务调节者)独立部署,负责事务的注册、回滚
  • 通过全局锁实现了写隔离和读隔离
存在问题

性能损耗

一条 Update 的 SQL,则需要全局事务 xid 获取(与 TC 通讯),before image(解析SQL,查询一次数据库),after image(查询一次数据库),insert undo log(写一次数据库)before commit(与 TC 通讯,判断锁冲空),这些提作都需要一次远程通讯 RPC,而目是同步的,另外undo log写入时blob字段的插入性能也是不高的,每条写 SQL 都会增加这么多开销,粗略估计会增加5倍响应时间

性价比
为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?按照二八原则预估,为了 20% 的交易回滚,需要将 80% 的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得?

全局锁

  • 热点数据
    相比 XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在 commit 前全局锁的判定也拉长了对数据锁的占有时间,这个开销比 XA 的 prepare 低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重

  • 回滚锁释放时间

    Seata 在回滚时,需要先删除各节点的 undo log,然后才能释放 TC 内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长

  • 死锁问题

    Seata 的引入全局锁会额外增加死锁的风险,但如果出现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不高效,也延长了对数据库锁的占有时间

6.4 Seata 快速开始

@GlobalTransactional

Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM 和 RM(Client端)由业务系统集成

Seata Server (TC)环境搭建

部署指南https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html

Server 端存储模式(store.mode)支持三种:

  • file(默认):单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认)
  • db:高可用模式,全局事务会话信息通过db共享,相对性能差些
  • redis:Seata-Server 1.3 及以上版本支持,性能较高,存在事务信息丢失的风险(需要提前配置适合当前场景的 redis 持久化配置)

资源目录:https://github.com/seata/seata/tree/1.3.0/script

  • client

存放 client 端 sql 脚本,参数配置

  • config-center

各个配置中心参数导入脚本,config.txt(包含 server 和 client,原名nacos-config.txt)为通用参数文件

  • server

server 端数据库脚本及各个容器配置

db存储模式 + Nacos(注册&配置中心)部署

步骤

  1. 下载安装包

https://github.com/seata/seata/releases

在这里插入图片描述

  1. \seata\conf路径下修改 file.conf文件

在这里插入图片描述

在这里插入图片描述

  1. 创建数据库seata

https://github.com/seata/seata/tree/1.3.0/script获取数据库seata_server的表

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

/script/server/db/mysql.sql

  • 建议将整个工程下载下来

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

6.5 Seata服务搭建—nacos

模式 + Nacos(注册&配置中心)部署

在这里插入图片描述

配置Nacos 注册中心 负责事务参与者(微服务)和 TC 通信

  1. 为实现db + Nacos高可用集群模式,需要修改/seata/conf目录下的registry.conf文件

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  1. 修改config.txt文件,在文件\seata\script\config-center

在这里插入图片描述

在这里插入图片描述

  1. 在 Windows 中启动 nacos-server 服务

在这里插入图片描述

  1. 打开git bush here没有的需要安装git),使用 git 启动文件nacos-config.sh,并进行参数化配置启动,启动后会将config.txt的配置添加到 nacos 注册中心上面(如果使用本地的Nacos,直接启动这个.sh文件就好了,不需要配置任何参数
sh ${SEATAPATH}/script/config-center/nacos/nacos-config.sh -h localhost -p 7070 -g SEATA_GROUP

参数说明

  • h:host,默认值 localhost
  • p:port,默认值8848
  • -g:配置分组,默认值为 SEATA_GROUP
  • -t:租户信息,对应 Nacos 的命名空间 ID 字段,默认为空

(如果使用 Windows跳过以下步骤)

问题一:由于启动nacos-config.sh该文件时,输出目录报错

cat: /d/Program: No such file or directory 
cat: Files/seata/script/config-center/config.txt: No such

文件名不能带有" "空格,否则就会输以上结果

问题二:在起初配置 Nacos 时,由于设置了集群模式,出现了一直尝试连接的错误

在这里插入图片描述

对此,我们对文件nacos-config.sh进行修改

在这里插入图片描述

  • 同时还要开启其它集群的Nacos注册中心

这里停止

  • 下面是执行成功的结果的截图

在这里插入图片描述

  1. 在 Nacos 注册中心查看文件配置情况

在这里插入图片描述

这里我们使用的时 Windows 上的 Nacos 服务,而数据库使用的是本地的 MySQL

  • 配置事务分组,要与客户端配置的事务分组一致
#default_tx_group 需要与客户端保持一致
#default 需要跟客户端和registry.conf中registry中的cluster值保持一致

(客户端 properties 配置:spring.cloud.alibaba.seata.tx-service-group=default_tx_group)

事务分组:异地机房停电容错机制

default_tx_group 可以自定义

在这里插入图片描述

TC 的异地多机房容灾

  • 假定TC集群部署在两个机房:guangzhou 机房(主)和 shanghai 机房(备)各两个实例
  • 一整套微服务架构项目:projectA
  • projectA内有微服务:serviceA、serviceB、serviceC 和 serviceD

其中,projectA所有微服务的事务分组 tx-service-group 设置为:projectA,projectA正常情况下使用 guangzhou 的 TC 集群(主)

那么正常情况下,client端的配置如下所示:

seata.tx-service-group=projectA
seata.service.vgroup-mapping.projectA=Guangzhou

在这里插入图片描述

假如此时 guangzhou 集群分组整个 down 掉,或者因为网络原因 projectA 暂时无法与 Guangzhou 机房通讯,那么我们将配置中心中的 Guangzhou 集群分组改为 Shanghai,如下:

seata.service.vgroup-mapping.projectA=Shanghai

并推送到各个微服务,便完成了对整个 projectA 项目的TC集群动态切换

在这里插入图片描述

启动Seata Server
  • 源码启动:执行 server 模块下 io.seata.server.Server.java 的 main 方法(使用这个就好)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 命令启动:
bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test

支持的启动参数

参数全写作用备注
-h–host指定在注册中心注册的IP不指定时获取当前的IP,外部访问部署在云环境和容器中的server建议指定
-p–port指定server启动的端口默认为8091
-m–storeMode事务日志存储方式支持 filedbredis,默认为file注意: redis 需要 seata-server 1.3 版本及以上
-n–serverNode用于指定seata-server节点ID如 1,2,3…,默认为1
-e–seataEnv指定seata-server运行环境devtest等,服务启动时会使用registry-dev.conf这样的配置

6.6 Seata—分布式事务搭建

Seata Client 快速开始

接入微服务应用 声明式事务实现(@GlobalTransactional)

  • 启动 Seata server 端,Seata server 使用 nacos 作为配置中心和注册中心

  • 配置微服务整合 seata

用户下单,整个业务逻辑由两个微服务构成

  • 订单服务:根据采购需求创建订单
  • 库存服务:对给定的商品扣除库存数量
  1. 在父工程 springcloud_alibaba 下创建一个 seata 模块

  2. 创建数据库seata_order并创建表order;创建数据库seata_stock并创建表stock

USE seata_order;
CREATE TABLE `order` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `product_id` INT(11) DEFAULT NULL,
  `total_amount` INT(11) DEFAULT NULL,
  `status` VARCHAR(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

USE seata_stock;
CREATE TABLE `stock` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `product_id` INT(11) DEFAULT NULL,
  `count` INT(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

在这里插入图片描述

本地事务测试
  1. seata 模块中添加依赖
  • pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud</artifactId>
        <groupId>com.vinjcent</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>seata</artifactId>
    <packaging>pom</packaging>

    <modules>
        <module>springcloud-provider-order-seata-8006</module>
        <module>springcloud-provider-stock-seata-8007</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--jdbc驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
    </dependencies>

</project>
  1. seata 模块下创建 springcloud-provider-order-seata-8006 模块
  • pom.xml对静态资源进行过滤
<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.yml</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.yml</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>
  • 编写 pojo 实体类
/**
 * @TableName order
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Order implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer id;

    private Integer productId;

    private Integer totalAmount;

    private String status;


}
  • 编写 OrderMapper.class 映射接口
@Mapper
@Repository
public interface OrderMapper {

    int addOrder(Order order);

    List<Order> getAllOrders();

    int deleteOrder(@Param("id") Integer id);

    int updateOrder(Order order);
}
  • 编写 OrderMapper.xml 映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vinjcent.springcloud.mapper.OrderMapper">

    <insert id="addOrder" parameterType="Order" useGeneratedKeys="true" keyProperty="id">
        insert into seata_order.order(product_id,total_amount,status)
        values (#{productId},#{totalAmount},#{status})
    </insert>

    <update id="updateOrder" parameterType="Order">
        update seata_order.order set product_id= #{productId},total_amount = #{totalAmount},status = #{status}
        where id = #{id}
    </update>

    <select id="getAllOrders" resultType="Order">
        select * from seata_order.order
    </select>

    <delete id="deleteOrder" parameterType="integer">
        delete from seata_order.order where id = #{id}
    </delete>

</mapper>
  • 业务逻辑层接口 OrderService.class 和实现类 OrderServiceImpl.class
public interface OrderService {

    int addOrder(Order order);

    List<Order> getAllOrders();

    int deleteOrder(Integer id);

    int updateOrder(Order order);

}


@SuppressWarnings("all")
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private OrderMapper orderMapper;

    @Transactional
    @Override
    public int addOrder(Order order) {

        int result = orderMapper.addOrder(order);

        // 扣减库存
        LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
        map.add("productId",order.getProductId());

        String msg = restTemplate.postForObject("http://localhost:8007/stock/reduct",map,String.class);

        // 模拟异常
        int a = 1/0;

        return result;
    }

    @Override
    public List<Order> getAllOrders() {
        return orderMapper.getAllOrders();
    }

    @Override
    public int deleteOrder(Integer id) {
        return orderMapper.deleteOrder(id);
    }

    @Override
    public int updateOrder(Order order) {
        return orderMapper.updateOrder(order);
    }
}

  • 配置类 ConfigBean.class
@Configuration
public class ConfigBean {   // @Configuration -->  相当于 spring 的 application.xml

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}
  • 控制类 OrderController.class
@SuppressWarnings("all")
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;  // 提供多种便捷访问远程http服务的方法,简单的Restful服务模板

    @Autowired
    private OrderService orderService;

    @RequestMapping("/add")
    public String addOrder(){

        Order order = new Order();
        order.setProductId(9)
                .setStatus("下单成功")
                .setTotalAmount(100);

        // 插入订单信息
        orderService.addOrder(order);

        return "下单成功";
    }

}
  • application.yml文件
# 端口号
server:
  port: 8006

# mybatis配置
mybatis:
  # 如果Mybatis"核心配置文件"与"接口映射文件"不在同一个包下,启动时会保存,必须舍去其中之一
  #  config-location: classpath:mybatis/mybatis-config.xml
  configuration:
    map-underscore-to-camel-case: true        # 开启驼峰命名
    cache-enabled: true       # 开启二级缓存
  type-aliases-package: com.vinjcent.springcloud.pojo
  mapper-locations: classpath:com/vinjcent/springcloud/mapper/xml/*.xml

# spring配置
spring:
  application:
    name: springcloud-provider-order-seata
  datasource: # 数据源
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456
  • 启动类
@SpringBootApplication
@EnableFeignClients
public class Order_Seata {
    public static void main(String[] args) {
        SpringApplication.run(Order_Seata.class, args);
    }
}
  1. seata模块下创建springcloud-provider-stock-seata-8007模块-
  • pom.xml对静态资源进行过滤
<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.yml</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.yml</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>
  • 编写 pojo 实体类
/**
 * @TableName stock
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Stock implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer id;

    private Integer productId;

    private Integer count;
    
}
  • 编写 StockMapper.class 映射接口
@Mapper
@Repository
public interface StockMapper {

    int addStock(Stock stock);

    List<Stock> getAllStocks();

    int deleteStock(@Param("id") Integer id);

    int updateStock(@Param("productId") Integer productId);
}
  • 编写 StockMapper.xml 映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vinjcent.springcloud.mapper.StockMapper">

    <insert id="addStock" parameterType="Stock">
        insert into seata_stock.stock(product_id,count)
        values (#{productId},#{count})
    </insert>

    <update id="updateStock" parameterType="Stock">
        update seata_stock.stock set count = `count` - 1
        where product_id = #{productId}
    </update>

    <select id="getAllStocks" resultType="Stock">
        select * from seata_stock.stock
    </select>

    <delete id="deleteStock" parameterType="integer">
        delete from seata_stock.stock where id = #{id}
    </delete>


</mapper>
  • 业务逻辑层接口 StockService.class 和实现类 StockServiceImpl.class
public interface StockService {

    int addStock(Stock stock);

    List<Stock> getAllStocks();

    int deleteStock(Integer id);

    int updateStock(Integer productId);

}


@SuppressWarnings("all")
@Service
public class StockServiceImpl implements StockService {

    @Autowired
    private StockMapper stockMapper;


    @Override
    public int addStock(Stock stock) {
        return stockMapper.addStock(stock);
    }

    @Override
    public List<Stock> getAllStocks() {
        return stockMapper.getAllStocks();
    }

    @Override
    public int deleteStock(Integer id) {
        return stockMapper.deleteStock(id);
    }

    @Override
    public int updateStock(Integer productId) {
        return stockMapper.updateStock(productId);
    }
}
  • 控制类 StockController.class
@SuppressWarnings("all")
@RestController
@RequestMapping("/stock")
public class StockController {

    @Autowired
    private StockService stockService;

    @RequestMapping("/reduct")
    public String reductStock(@RequestParam(value = "productId") Integer productId){
        stockService.updateStock(productId);
        return "扣减库存成功!";
    }

}
  • application.yml配置文件
# 端口号
server:
  port: 8007

# mybatis配置
mybatis:
  # 如果Mybatis"核心配置文件"与"接口映射文件"不在同一个包下,启动时会保存,必须舍去其中之一
  #  config-location: classpath:mybatis/mybatis-config.xml
  configuration:
    map-underscore-to-camel-case: true        # 开启驼峰命名
    cache-enabled: true       # 开启二级缓存
  type-aliases-package: com.vinjcent.springcloud.pojo
  mapper-locations: classpath:com/vinjcent/springcloud/mapper/xml/*.xml

# spring配置
spring:
  application:
    name: springcloud-provider-stock-seata
  datasource: # 数据源
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/seata_stock?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456

测试

  1. 启动 springcloud-provider-order-seata-8006

  2. 启动 springcloud-provider-stock-seata-8007

  3. 访问http://localhost:8006/order/add

在这里插入图片描述

在这里插入图片描述

问题:@Transactional本地事务无法解决分布式事务的场景

分布式事务测试
  1. springcloud-provider-order-seata-8006 模块和 springcloud-provider-stock-seata-8007 模块分别添加依赖
<dependencies>
    <!--nacos-服务注册与发现-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--seata依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
</dependencies>
  1. 对两个模块的application.yml添加 nacos 配置
# 端口号
server:
  port: 8006

# mybatis配置
mybatis:
  # 如果Mybatis"核心配置文件"与"接口映射文件"不在同一个包下,启动时会保存,必须舍去其中之一
  # config-location: classpath:mybatis/mybatis-config.xml
  configuration:
    map-underscore-to-camel-case: true    # 开启驼峰命名
    cache-enabled: true                   # 开启二级缓存
  type-aliases-package: com.vinjcent.springcloud.pojo   # 实体类别名
  mapper-locations: classpath:com/vinjcent/springcloud/mapper/xml/*.xml   # xxxMapper.xml 文件

# spring配置
spring:
  application:
    # 应用服务名
    name: springcloud-provider-order-seata
  datasource: # 数据源
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456
  cloud:
    # Nacos 注册中心配置
    nacos:
      discovery:
        # nacos服务地址
        server-addr: 127.0.0.1:8848
        username: nacos
        password: nacos
    alibaba:
      seata:
        # 对应 service.vgroupMapping.guangzhou=default
        tx-service-group: guangzhou   # 配置事务分组,由于在config.txt中设置集群部署机房在"guangzhou",所以这里要修改
# Seata配置
seata:
  enabled: true
  application-id: ${spring.application.name}  # 微服务应用名称
  tx-service-group: guangzhou                 # 此处配置自定义的seata事务分组名称
  enable-auto-data-source-proxy: true         # 开启数据库代理
  # 配置seata的注册中心,告诉seata client(参与者) 怎么去访问seata server(TC---事务协调者)
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      application: seata-server   # seata服务名,默认是seata-server
      username: nacos
      password: nacos
      group: SEATA_GROUP  # seata服务分组,默认是SEATA_GROUP
      # 对应registry.conf文件中的       cluster = "default"
      cluster: default #默认集群名
  # 配置seata的配置中心
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
      group: SEATA_GROUP
  # 配置微服务的事务分组
  service:
    vgroup-mapping:
      gaungzhou: default
    grouplist:
      # 启动seata-server.bat 中的seata-server的端口号
      default: 127.0.0.1:8091
    enable-degrade: false
    disable-global-transaction: false
        
        
# =========================================================================================
# 端口号
server:
  port: 8007

# mybatis配置
mybatis:
  # 如果Mybatis"核心配置文件"与"接口映射文件"不在同一个包下,启动时会保存,必须舍去其中之一
  # config-location: classpath:mybatis/mybatis-config.xml
  configuration:
    map-underscore-to-camel-case: true    # 开启驼峰命名
    cache-enabled: true                   # 开启二级缓存
  type-aliases-package: com.vinjcent.springcloud.pojo   # 实体类别名
  mapper-locations: classpath:com/vinjcent/springcloud/mapper/xml/*.xml   # xxxMapper.xml 文件

# spring配置
spring:
  application:
    # 应用服务名
    name: springcloud-provider-stock-seata
  datasource: # 数据源
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/seata_stock?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456
  cloud:
    # Nacos 注册中心配置
    nacos:
      discovery:
        # nacos服务地址
        server-addr: 127.0.0.1:8848
        username: nacos
        password: nacos
    alibaba:
      # 配置所使用seata的事务分组
      seata:
        # 对应 service.vgroupMapping.guangzhou=default
        tx-service-group: guangzhou   # 配置事务分组,由于在config.txt中设置集群部署机房在"guangzhou",所以这里要修改
# Seata配置
seata:
  enabled: true
  application-id: ${spring.application.name}  # 微服务应用名称
  tx-service-group: guangzhou                 # 此处配置自定义的seata事务分组名称
  enable-auto-data-source-proxy: true         # 开启数据库代理
  # 配置seata的注册中心,告诉seata client(参与者) 怎么去访问seata server(TC---事务协调者)
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      application: seata-server   # seata服务名,默认是seata-server
      username: nacos
      password: nacos
      group: SEATA_GROUP  # seata服务分组,默认是SEATA_GROUP
      # 对应registry.conf文件中的       cluster = "default"
      cluster: default #默认集群名
  # 配置seata的配置中心
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
      group: SEATA_GROUP
  # 配置微服务的事务分组
  service:
    vgroup-mapping:
      gaungzhou: default
    grouplist:
      # 启动seata-server.bat 中的seata-server的端口号
      default: 127.0.0.1:8091
    enable-degrade: false
    disable-global-transaction: false

在这里插入图片描述

在这里插入图片描述

参数配置信息:https://seata.io/zh-cn/docs/user/configurations.html

  1. springcloud-provider-order-seata-8006 模块中编写api接口 StockService.class
@SuppressWarnings("all")
@FeignClient(value = "springcloud-provider-stock-seata",path = "/stock")
public interface StockService {

    @RequestMapping("/reduct")
    public String reductStock(@RequestParam(value = "productId") Integer productId);

}
  1. 修改 springcloud-provider-order-seata-8006 模块中的OrderServiceImpl.class
@SuppressWarnings("all")
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private StockService stockService;

    @Autowired
    private OrderMapper orderMapper;

    @GlobalTransactional
    @Override
    @GlobalLock
    public int addOrder(Order order) {

        int result = orderMapper.addOrder(order);

        // 扣减库存
        stockService.reductStock(order.getProductId());

        // 模拟异常
        int a = 1/0;

        return result;
    }

    @Override
    public List<Order> getAllOrders() {
        return orderMapper.getAllOrders();
    }

    @Override
    public int deleteOrder(Integer id) {
        return orderMapper.deleteOrder(id);
    }

    @Override
    public int updateOrder(Order order) {
        return orderMapper.updateOrder(order);
    }
}
  1. 再主动类中开启 Openfeign 注解
@SpringBootApplication
@EnableTransactionManagement
@EnableFeignClients
public class Order_Seata {
    public static void main(String[] args) {
        SpringApplication.run(Order_Seata.class, args);
    }
}
  1. 各微服务对应数据库中添加undo_log
CREATE TABLE `undo_log` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `branch_id` BIGINT(20) DEFAULT NULL,
  `xid` VARCHAR(100) DEFAULT NULL,
  `context` VARCHAR(128) DEFAULT NULL,
  `rollback_info` LONGBLOB,
  `log_status` INT(11) DEFAULT NULL,
  `log_created` DATETIME DEFAULT NULL,
  `log_modified` DATETIME DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `xid` (`xid`),
  UNIQUE KEY `branch_id` (`branch_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8

在这里插入图片描述

测试

  1. 运行本地 Nacos 服务
  2. 运行\seata\bin目录下的seata-server.bat服务
  3. 启动 springcloud-provider-order-seata-8006
  4. 启动 springcloud-provider-stock-seata-8007

数据库数据没有任何变化

Debug原理

如果报错:无法连接到 seata-server

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

需要重新运行\seata\bin目录下的seata-server.bat服务

在这里插入图片描述

在这里插入图片描述

  • global_table 就是存储的全局事务信息,一进入事务方法中就会生成 xid

在这里插入图片描述

  • branch_table 存储的是参与者,当运行数据库操作方法,就会存储事务分支信息

在这里插入图片描述

在这里插入图片描述

  • lock_table 当前正在锁的表的一条记录

在这里插入图片描述

  • order表的回滚日志

在这里插入图片描述

  • stock表的回滚日志

在这里插入图片描述

下一篇文章微服务分布式组件—Gateway

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Cloud Alibaba是一个以Spring Cloud为基础的微服务开发框架,它提供了一站式的微服务解决方案,包括服务注册与发现、配置中心、消息总线、负载均衡、断路器、数据监控等组件,同时还提供了阿里中间件的支持,例如阿里云的RocketMQ、分布式事务解决方案Seata等。使用Spring Cloud Alibaba可以快速搭建分布式应用系统,提高开发效率和系统可靠性。 下面是一个使用Spring Cloud Alibaba实现微服务的例子: 1.添加依赖 ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.2.3.RELEASE</version> </dependency> ``` 2.配置文件 ```yaml spring: application: name: service-provider cloud: nacos: discovery: server-addr: localhost:8848 # Nacos服务注册中心地址 ``` 3.编写服务提供者 ```java @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, World!"; } } ``` 4.启动服务提供者 ```java @SpringBootApplication @EnableDiscoveryClient public class ServiceProviderApplication { public static void main(String[] args) { SpringApplication.run(ServiceProviderApplication.class, args); } } ``` 5.编写服务消费者 ```java @RestController public class HelloController { @Autowired private RestTemplate restTemplate; @GetMapping("/hello") public String hello() { String url = "http://service-provider/hello"; return restTemplate.getForObject(url, String.class); } } ``` 6.启动服务消费者 ```java @SpringBootApplication @EnableDiscoveryClient public class ServiceConsumerApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ServiceConsumerApplication.class, args); } } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Naijia_OvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值