Seata分布式事务

本地事务

数据库事务(简称:事务),Transactional是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
起初,事务仅限于对单一数据库资源的访问控制,架构服务话以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及到一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
image.png

事务特性

原子性(Atomicity):事务作为一个整体去执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一状态。一致状态是指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态下不能被观察到(这层语义也应该属于原子性)。
**隔离性(lsolation):**多个事务并发执行时,一个事务的执行不应影响其他事物的执行,如同只有这一个操作在被数据库所执行一样。
执行性(Durability):已被提交的事物对数据库的修改应该是永久性的保存在数据库中。在事务结束后,此操作不可逆转。

分布式事务

分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
最早的分布式事务应用架构很简单,不涉及服务间的访问调用,仅仅是服务内操作涉及到对多个数据库资源的访问。
image.png
对于上面介绍的分布式事务应用架构,尽管—个服务操作会访问多个数据库资源,但是毕竟整个事务还是控制在单一服务的内部。如果一个服务操作需要调用另外一个服务,这时的事务就需要跨越多个服务了。在这种情况下,起始于某个服务的事务在调用另外一个服务的时候,需要以某种机制流转到另外一个服务,从而使被调用的服务访问的资源也自动加入到该事务当中来。下图反映了这样一个跨越多个服务的分布式事务:
image.png
如果将上面这两种场景(—个服务可以调用多个数据库资源,也可以调用其他服务)结合在一起,对此进行延伸,整个分布式事务的参与者将会组成如下图所示的树形拓扑结构。在一个跨服务的分布式事务中,事务的发起者和提交均系同一个,它可以是整个调用的客户端,也可以是客户端最先调用的那个服务。
image.png

分布式事务相关理论

CAP定理

image.png
CAP定理是在1998年加州大学计算机科学家EricBrewer提出的,分布式系统有3个指标

  • Consistency 一致性
  • Availiability 可用性
  • Partition tolerance 分区容错性

这三个指标首字母加起来就是CAP,但是这不可能同时做到CAP,这个结论就叫做CAP定理。

分区容错性

大多数分布式系统都分不在多个子网络中,每个子网络就叫做一个区。分区容错性的意思就是,区间通信可能失败。比如,一台服务器放在中国,一台放在美国,这就是两个区,她们之间可能无法通信。
image.png
G1和G2两台跨区服务器。G1向G2发送一条消息,G2可能无法收。系统设计时,必须考虑该情况。
一般来说,分区容错无法避免,因此认为,CAP的P总是成立的,C和A不能同时做到。

可用性

可用性,只要接收到用户的请求,服务器就必须给出回应。
用户可以选择向G1或G2发起读操作,不管是哪台服务器,只要收到请求,就必须告诉用户,到底是V0还是V1,否则就不满足可用性。但是这样可能会导致数据不准确。
image.png

一致性

写操作之后的读操作,必须返回该值。

  • 强一致性,要求更新过的数据能被后续的访问都能看到
  • 弱一致性,能容忍后续的部分或者全部访问不到
  • 最终一致性,经过一段时间之后,要求能访问到更新后的数据

CAP中的一致性是指强一致性。
eg:某条记录是V0,用户向G1发起一个读操作,将其改为V1.
image.png
用户有可能向G2发起读操作,由于G2没有发生变化,因此返回的还是V0。
G1和G2的读操作结果不一致,这就不满足一致性了。
image.png
为了让G1也能变为V1,就要在G1写操作时,让G1发送一条消息给G2,要求G2也改为V1
image.png
但是同步数据时,可能网络故障,数据就不能保持一致性,要么就是等待服务器恢复后再去请求,但是这样就不能保证可用性了。

  • AP:保证可用性,放弃一致性,返回的数据有可能不准确。(12306)
  • CP:保证一致性,放弃可用性,等待网络恢复后,再同步返回数据,保证数据准确。(ZooKeeper),与钱相关的一般都是保证强一致性。
BASE理论

BASE:全称: Basically Available(基本可用),Soft state(软状态) 和 Eventuallv consistent (最终一致性)三个短语的缩写,来自ebpy 的架构师提出。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:

即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务待点,采用适当的方式来使系统达到最终一致性 (Eventual consistency)
eg:类似于12306,查看票时,有票但是一下单就没票了,这样可以保证可用性的同时保证一致性。

Basically Available(基本可用)
什么是基本可用呢?假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言:
1,响应时间上的损失:正常情况下的搜索引擎 0.5 秒即返回给用户结果,而基本可用的搜索引擎可以在1秒左右返回结果。
2. 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单,但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。到证了基本可用。
Soft state (软状态)
什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually consistent (最终—致性)
系统能路保证在没有其他新的更新操作的情况下,致据地终一定能够达到一致的状态,因此所有客户端对系统的数据访问最终都能获取到最新的值。

分布式事务解决方案
基于XA协议两阶段提交

image.png
XA规范中分布式事务有AP、RM、TM组成:

  • 应用程序AP:定义事务边界(定义事务的开始和结束)并访问事务边界内的资源。
  • 资源管理器RM:RM管理计算机的共享资源,许多软件都可以去访问这些资源,资源包含比如数据库、问价系统、打印机服务等。
  • 事务管理器TM:负责全局事务,分配全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、会滚、失败恢复等等。

第一阶段:TM要求所有的RM准备自己对应的事务分支,询问每个RM能否成功提交当前的事务分支,RM根据自己的情况返回TM状态,失败返回NO,RM就会对这个分支就行会滚,当前的这个分支就没了
第二阶段:TM根据第一阶段提交的结果就行处理,是提交还是会滚,所有的RM都返回成功,TM会通知所有的RM提交事务,只要有一个RM返回NO,则通知所有的RM会滚事务。
优点:尽量保证了数据的强一致性,适合对数据强一致要求很高的关键领域,(其实也不能100%保证强一致性)
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发场景。

TCC事务补偿机制

TCC其实就是采用补偿机制,其核心思想是:针对每个操作,都要注册一个与其对一个的确认和补偿(撤销)操作,分为3阶段:

  • Try阶段主要是对业务系统做检测及资源预留
  • Confirm阶段主要是对业务系统做确认提交,try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的,只要try成功,confirm一定成功
  • Cancel阶段主要是在业务执行错误,需要会滚的状态下执行的业务取消,预留资源释放

image.png
举例:A和B转账
我们有一个本地方法,里面依次调用
1、首先在try阶段,要先调用远程接口把B和A的钱冻结起来。
2、在confirm阶段,执行远程调用的转账操作,转账成功解冻。
3、在执行第2步成功时,那么转账成功,如果执行失败,则调用远程接口对应的解冻方法(Cancel)
流程
1、Try接口,先查看A账户余额是否充足,充足则冻结金额,并扣减。B账户需要检查账户是否可用
2、刚才的操作都没问题,就需要去走Confirm接口:A:刚才冻结的金额,解冻并真正扣减;B:把对应的钱加起来。这样事务就提交成功了。
3、如果Try接口中任何一方有异常,就不会走Confirm接口,走Cancel接口,A:把金额解冻回来,并且增加回来,将数据进行会滚操作。

  • 优点:相比两阶段提交,可用性比较强
  • 缺点:数据的一致性要差些,TCC属于应用层面的一种补偿机制,所以需要程序员在实现的时候,多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不好定义及处理。代码侵入性很强。
消息最终一致性

消息最终一致性核心思想是将分布式事务拆成本地事务进行处理,这种思想来源ebay,示意图:
image.png
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

  • 优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性。
  • 缺点:消息表会男合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理

分布式事务问题简介

跨数据库、多数据源的统一调度。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立应用,分别使用三个独立的数据源,
业务操作需要调用3个服务来完成。此时每个服务内部的数据一致性由本地事物保证,但是全局的数据一致性问题无法得到保证!
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
示例:
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供技术支持:

  • 仓储服务:对给定的商品扣除仓储数量
  • 订单服务:根据采购需要创建订单
  • 账户服务:从用户账户中扣除余额

架构图:
image.png

Seata简介

官网地址
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务解决方案

Seata术语

Transcation ID XID-全局事物ID

TC (Transaction Coordinator) - 事务协调者

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

TM (Transaction Manager) - 事务管理器

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

RM (Resource Manager) - 资源管理器

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

分布式事务处理过程

处理过程

1、TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
2、XID在微服务调用链路的上下文中传播;
3、RM向TC注册分支事务,将其纳入XID对应的全局事务的管辖;
4、TM向TC发起针对XID的全局提交或回滚决议;
5、TC调度XID下管辖的全局分支事务完成提交或回滚请教;

示意图

image.png

Seata-Server安装

发布地址
1.0.0tar下载地址
GitHub官网下载换缓慢,可以通过github官网下载加速器加速下载

上传tar到服务器

解压
tar -zxvf seata-server-1.0.0.tar.gz 

修改配置文件

修改conf\file.conf文件
先备份,再做修改

cp file.conf file.conf.bak

主要修改自定义事务组名称+事务日志存储模式为DB+数据库连接信息
事务日志存储模式默认为文件,修改为存储到DB中

修改service模块

原厂默认配置:

service {
  #transaction service group mapping
  vgroup_mapping.my_test_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #disable seata
  disableGlobalTransaction = false
}

修改vgroup_mapping.my_test_tx_group的value,自定义即可

service {
  #transaction service group mapping
  vgroup_mapping.my_test_tx_group = "seata_tx_group"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #disable seata
  disableGlobalTransaction = false
}
修改store模块
## transaction log store, only used in seata-server
store {
  ## store mode: file、db
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "xxxxx"
  }
}

修改详情:
image.png

建数据库

由于我们将存储模式从文件修改为了db,所以我们需要创建对应的数据库

-- 创建seata库
CREATE DATABASE IF NOT EXISTS seata;

0.9版本建表SQL存储在db_store.sql在seata目录下的conf目录里面
但是我的1.0.0版本conf目录下,并没有对应的sql文件,查看官网:
image.png
去github上获取对应的sql语句
1.0.0版本SQL

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME,
    `gmt_modified`      DATETIME,
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

guthub:https://github.com/seata/seata/blob/1.0.0/script/server/db/mysql.sql
image.png
将此脚本直接执行即可生成对应的表文件
image.png

修改registry文件

先做备份

cp registry.conf registry.conf.bak

原文件:

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    cluster = "default"
  }
...
}

修改为nacos,并配置nacos的信息

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
  }
...
}

image.png

启动

先启动Nacos,等Nacos启动完成,再启动Seata
负责会启动报错:
image.png

启动Nacos
sh /root/development/nacos/bin/startup.sh -m standalone
启动Seata
sh /root/development/seata/bin/seata-server.sh

查看启动结果:
image.png

Seata分布式交易

分布式业务说明

创建3个微服务,一个订单服务,一个库存服务,一个账户服务
当用户下订单时,会在订单服务中创建一个订单,然后通过远程调用来库存来扣件下单商品库存,
再通过远程调用账户服务来扣件用户账户里的余额。
最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,存在分布式事务问题。
下订单---->减库存---->扣余额---->改状态

业务库准备

创建业务库

准备订单、库存、账户业务数据库准备

  • seata_order:存储订单的数据库
  • seata_storage:存储账户的数据库
  • seata_account:存储账户信息的数据库

对应的建库语句:

CREATE DATABASE IF NOT EXISTS `seata_order`;
CREATE DATABASE IF NOT EXISTS `seata_storage`;
CREATE DATABASE IF NOT EXISTS `seata_account`;

业务表:

USE seata_order;
CREATE TABLE IF NOT EXISTS t_order(
	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
	`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
	`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
	`count` INT(11) DEFAULT NULL COMMENT '数量',
	`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
	`status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
USE seata_storage;
CREATE TABLE IF NOT EXISTS t_storage(
	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
	`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
	`total` INT(11) DEFAULT NULL COMMENT '总库存',
	`userd` INT(11) DEFAULT NULL COMMENT '已用库存',
	`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- 添加测试数据
INSERT INTO t_storage VALUES(NULL,1,100,0,100);
USE seata_account;
CREATE TABLE IF NOT EXISTS t_account(
	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
	`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
	`total` INT(11) DEFAULT NULL COMMENT '总额度',
	`userd` INT(11) DEFAULT NULL COMMENT '已用余额',
	`residue` INT(11) DEFAULT NULL COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- 添加测试数据
INSERT INTO t_account VALUES(NULL,1,1000,0,1000);
创建对应的回滚日志表
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
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',
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of undo_log
-- ----------------------------

创建结果:
image.png

微服务搭建

seata-order微服务搭建

pom

<?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>seata-demo</artifactId>
        <groupId>com.xiu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>

    <artifactId>seata-order</artifactId>

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

    <dependencies>
        <dependency>
            <groupId>com.xiu</groupId>
            <artifactId>seata-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

</project>

application.yml

server:
  port: 2001
spring:
  application:
    name: seata-order
  cloud:
    alibaba:
      seata:
        tx-service-group: seata_tx_group #事务组名称与seata配置file.conf文件配置的保持一致
    nacos:
      discovery:
        server-addr: 172.16.138.100:8848
  datasource: #数据库配置信息
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://172.16.138.100:3306/seata_order
    username: root
    password: xxxxx

feign:
  hystrix:
    enabled: false
logging:
  level:
    io:
      seata: info

启动类:

package com.xiu.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * Order订单模块主启动类
 * 取消数据源自动创建
 *
 * @author zhangzengxiu
 * @date 2023/2/19
 */
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class OrderApp {
    public static void main(String[] args) {
        SpringApplication.run(OrderApp.class, args);
    }
}

OrderService接口

import com.xiu.common.model.order.domain.Order;

/**
 * @author zhangzengxiu
 * @date 2023/2/19
 */
public interface OrderService {

    /**
     * 创建新订单
     *
     * @param order
     * @return
     */
    boolean createOrder(Order order);

}

实现类:

import com.xiu.common.model.order.domain.Order;
import com.xiu.common.model.order.enums.OrderStatusEnum;
import com.xiu.order.feign.AccountFeign;
import com.xiu.order.feign.StorageFeign;
import com.xiu.order.mapper.OrderMapper;
import com.xiu.order.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author zhangzengxiu
 * @date 2023/2/19
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private AccountFeign accountFeign;

    @Autowired
    private StorageFeign storageFeign;

    @GlobalTransactional(name = "seata_tx_group", rollbackFor = Exception.class)
    @Override
    public boolean createOrder(Order order) {
        //创建新订单
        orderMapper.addOrder(order);
        //库存扣减
        storageFeign.decreaseStorage(order.getProductId(), order.getCount());
        //账户扣减
        accountFeign.decreaseAccount(order.getUserId(), order.getMoney());
        //修改订单状态
        orderMapper.updateOrderStatus(order.getId(), OrderStatusEnum.END.getStatusCode());
        return true;
    }
}

mapper

package com.xiu.order.mapper;

import com.xiu.common.model.order.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

/**
 * @author zhangzengxiu
 * @date 2023/2/19
 */
@Mapper
@Repository
public interface OrderMapper {

    /**
     * 创建新订单
     *
     * @param order
     * @return
     */
    Integer addOrder(Order order);

    /**
     * 修改订单状态
     *
     * @param id     订单id
     * @param status 订单新状态
     * @return
     */
    Integer updateOrderStatus(@Param("id") Long id, @Param("status") Integer status);

}

mapper.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.xiu.order.mapper.OrderMapper">

    <resultMap id="BaseResultMap" type="com.xiu.common.model.order.domain.Order">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="user_id" jdbcType="BIGINT" property="userId" javaType="long"/>
        <result column="product_id" jdbcType="BIGINT" property="productId" javaType="long"/>
        <result column="count" jdbcType="INTEGER" property="count" javaType="int"/>
        <result column="money" jdbcType="DECIMAL" property="money" javaType="bigDecimal"/>
        <result column="status" jdbcType="INTEGER" property="status" javaType="int"/>
    </resultMap>

    <!--创建新订单-->
    <insert id="addOrder" parameterType="com.xiu.common.model.order.domain.Order" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO t_order
        (id,
         user_id,
         product_id,
         count,
         money,
         status)
        VALUES
        (
         NULL,
         #{userId},
         #{productId},
         #{count},
         #{money},
         #{status}
         )
    </insert>

    <!--修改订单状态-->
    <update id="updateOrderStatus">
        UPDATE t_order
        SET status = #{status}
        WHERE id = #{id}
    </update>
</mapper>

controller

import com.xiu.common.CommonResult;
import com.xiu.common.model.order.domain.Order;
import com.xiu.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/createOrder")
    public CommonResult createOrder(@RequestBody Order order) {
        boolean res = orderService.createOrder(order);
        if (res) {
            return new CommonResult(200, "下订单成功", null);
        }
        return new CommonResult(444, "下订单失败", null);
    }
}

config

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author zhangzengxiu
 * @date 2023/2/19
 */
@Configuration
@ComponentScan("com.xiu.common.config")
public class SeataConfig {
}

feign

import com.xiu.common.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

/**
 * @author zhangzengxiu
 * @date 2023/2/20
 */
@FeignClient("seata-account")
public interface AccountFeign {

    /**
     * 账户扣减
     *
     * @param userId 账户id
     * @param money  扣减金额
     * @return
     */
    @PostMapping("/account/decreaseAccount")
    CommonResult decreaseAccount(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);

}
import com.xiu.common.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author zhangzengxiu
 * @date 2023/2/20
 */
@FeignClient("seata-storage")
public interface StorageFeign {

    /**
     * 库存扣减
     *
     * @param productId 产品id
     * @param count     扣减库存数量
     * @return
     */
    @PostMapping("/storage/decreaseStorage")
    CommonResult decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);

}
seata-storage微服务搭建

pom

<artifactId>seata-storage</artifactId>

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

    <dependencies>
        <dependency>
            <groupId>com.xiu</groupId>
            <artifactId>seata-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

application.yml

server:
  port: 2002
spring:
  application:
    name: seata-storage
  cloud:
    alibaba:
      seata:
        tx-service-group: seata_tx_group #事务组名称与seata配置file.conf文件配置的保持一致
    nacos:
      discovery:
        server-addr: 172.16.138.100:8848
  datasource: #数据库配置信息
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://172.16.138.100:3306/seata_storage
    username: root
    password: xxxxx

feign:
  hystrix:
    enabled: false
logging:
  level:
    io:
      seata: info

启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 库存服务主启动类
 *
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class StorageApp {
    public static void main(String[] args) {
        SpringApplication.run(StorageApp.class, args);
    }
}

service

import com.xiu.storage.mapper.StorageMapper;
import com.xiu.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@Service
public class StorageServiceImpl implements StorageService {

    @Autowired
    private StorageMapper storageMapper;

    @Override
    public boolean decreaseStorage(Long productId, Integer count) {
        int res = storageMapper.decreaseStorage(productId, count);
        return res == 1;
    }
}

mapper

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@Mapper
@Repository
public interface StorageMapper {

    /**
     * 库存扣减
     *
     * @param productId 产品id
     * @param count     扣减数量
     * @return
     */
    int decreaseStorage(@Param("productId") Long productId, @Param("count") Integer count);

}

mapper.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.xiu.storage.mapper.StorageMapper">
    <resultMap id="BaseResultMap" type="com.xiu.common.model.storage.domain.Storage">
        <id column="id" jdbcType="BIGINT" javaType="long" property="id"/>
        <result column="product_id" jdbcType="BIGINT" javaType="long" property="productId"/>
        <result column="total" jdbcType="INTEGER" javaType="int" property="total"/>
        <result column="userd" jdbcType="INTEGER" javaType="int" property="userd"/>
        <result column="residue" jdbcType="INTEGER" javaType="int" property="residue"/>
    </resultMap>

    <!--库存扣减-->
    <update id="decreaseStorage">
        update t_storage
        set userd = userd + #{count}, residue = residue - #{count}
        where product_id = #{productId}
    </update>


</mapper>

controller

import com.xiu.common.CommonResult;
import com.xiu.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@RestController
public class StorageController {

    @Autowired
    private StorageService storageService;

    @PostMapping("/storage/decreaseStorage")
    public CommonResult decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {
        boolean res = storageService.decreaseStorage(productId, count);
        if (res) {
            return new CommonResult(200, "库存扣减成功", null);
        }
        return new CommonResult(444, "库存扣减失败", null);
    }

}

config

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@Configuration
@ComponentScan("com.xiu.common.config")
public class SeataConfig {
}
seata-account微服务搭建

pom

<?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>seata-demo</artifactId>
        <groupId>com.xiu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata-account</artifactId>

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

    <dependencies>
        <dependency>
            <groupId>com.xiu</groupId>
            <artifactId>seata-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

</project>

application.yml

server:
  port: 2003
spring:
  application:
    name: seata-account
  cloud:
    alibaba:
      seata:
        tx-service-group: seata_tx_group #事务组名称与seata配置file.conf文件配置的保持一致
    nacos:
      discovery:
        server-addr: 172.16.138.100:8848
  datasource: #数据库配置信息
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://172.16.138.100:3306/seata_account
    username: root
    password: xxxxx

feign:
  hystrix:
    enabled: false
logging:
  level:
    io:
      seata: info

启动类

package com.xiu.account;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class AccountApp {
    public static void main(String[] args) {
        SpringApplication.run(AccountApp.class, args);
    }
}

service

package com.xiu.account.service.impl;

import com.xiu.account.mapper.AccountMapper;
import com.xiu.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public boolean decreaseAccount(Long userId, BigDecimal money) {
        try {
            //模拟业务超时
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int res = accountMapper.decreaseAccount(userId, money);
        return res == 1;
    }
}

mapper

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@Mapper
@Repository
public interface AccountMapper {

    /**
     * 账户扣减
     *
     * @param userId 用户id
     * @param money  扣减金额
     * @return
     */
    int decreaseAccount(@Param("userId") Long userId, @Param("money") BigDecimal money);

}

mapper.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.xiu.account.mapper.AccountMapper">

    <!--账户扣减-->
    <update id="decreaseAccount">
        update t_account
        set userd = userd + #{money}, residue = residue - #{money}
        where user_id = #{userId}
    </update>
</mapper>

controller

package com.xiu.account.controller;

import com.xiu.account.service.AccountService;
import com.xiu.common.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@RestController
public class AccountController {

    @Autowired
    private AccountService accountService;

    /**
     * 账户扣减
     *
     * @param userId 账户id
     * @param money  扣减金额
     * @return
     */
    @PostMapping("/account/decreaseAccount")
    public CommonResult decreaseAccount(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
        boolean res = accountService.decreaseAccount(userId, money);
        if (res) {
            return new CommonResult(200, "账户扣减成功", null);
        }
        return new CommonResult(444, "账户扣减失败", null);
    }
}

config

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@Configuration
@ComponentScan("com.xiu.common.config")
public class SeataConfig {
}
seata-common模块
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * @author zhangzengxiu
 * @date 2023/2/19
 */
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource getDruidDataSource() {
        return new DruidDataSource();
    }

    /**
     * 创建数据源代理
     * Primary:防止本地事务失效
     *
     * @param dataSource
     * @return
     */
    @Primary
    @Bean
    public DataSourceProxy getDataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory getSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*.xml"));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }


}

storage

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author zhangzengxiu
 * @date 2023/2/19
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Storage implements Serializable {
    private static final long serialVersionUID = 7103617841362037153L;

    /**
     * 主键id
     */
    private Long id;

    /**
     * 产品id
     */
    private Long productId;

    /**
     * 总库存
     */
    private Integer total;

    /**
     * 已用库存
     */
    private Integer userd;

    /**
     * 剩余库存
     */
    private Integer residue;

}

account

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author zhangzengxiu
 * @date 2023/2/19
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Serializable {
    private static final long serialVersionUID = -1515870651167229775L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 总额度
     */
    private Integer total;

    /**
     * 已用余额
     */
    private Integer userd;

    /**
     * 剩余可用额度
     */
    private Integer residue;

}

order

import com.xiu.common.model.order.enums.OrderStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * @author zhangzengxiu
 * @date 2023/2/19
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {

    /**
     * 主键id
     */
    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 产品id
     */
    private Long productId;

    /**
     * 数量
     */
    private Integer count;

    /**
     * 金额
     */
    private BigDecimal money;

    /**
     * 订单状态
     *
     * @see OrderStatusEnum
     */
    private Integer status;

}

枚举

import java.util.Objects;

/**
 * 订单状态枚举类
 *
 * @author zhangzengxiu
 * @date 2023/2/19
 */
public enum OrderStatusEnum {

    CREATING(0, "创建中"), END(1, "已完结");

    /**
     * 订单状态编码
     */
    private Integer statusCode;

    /**
     * 订单状态
     */
    private String status;

    OrderStatusEnum(Integer statusCode, String status) {
        this.statusCode = statusCode;
        this.status = status;
    }

    public Integer getStatusCode() {
        return statusCode;
    }

    public String getStatus() {
        return status;
    }

    /**
     * 根据订单状态编码获取订单状态
     *
     * @param statusCode
     * @return
     */
    public static String getStatusByCode(Integer statusCode) {
        if (Objects.equals(statusCode, null)) {
            return null;
        }
        OrderStatusEnum[] values = values();
        if (values == null || values.length <= 0) {
            return null;
        }
        for (OrderStatusEnum orderStatusEnum : values) {
            if (orderStatusEnum == null) {
                continue;
            }
            if (Objects.equals(orderStatusEnum.getStatus(), statusCode)) {
                return orderStatusEnum.getStatus();
            }
        }
        return null;
    }

}

注意事项

  • seata-spring-boot-starter

内置GlobalTransactionScanner自动初始化功能,若外部实现初始化,请参考SeataAutoConfiguration保证依赖加载顺序 默认开启数据源自动代理,可配置seata.enable-auto-data-source-proxy: false关闭

  • spring-cloud-starter-alibaba-seata

查看版本说明 2.1.0内嵌seata-all 0.7.1,2.1.1内嵌seata-all 0.9.0,2.2.0内嵌seata-spring-boot-starter 1.0.0, 2.2.1内嵌seata-spring-boot-starter 1.1.0

2.1.0和2.1.1兼容starter解决方案: @SpringBootApplication注解内exclude掉spring-cloud-starter-alibaba-seata内的com.alibaba.cloud.seata.GlobalTransactionAutoConfiguration 
  • spring-cloud-starter-alibaba-seata推荐依赖配置方式
           <dependency>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
                <version>最新版</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
                <version>最新版本</version>
                <exclusions>
                    <exclusion>
                        <groupId>io.seata</groupId>
                        <artifactId>seata-spring-boot-starter</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>

file.conf
将seata目录conf下的file.conf.example文件拷贝到资源目录中,该文件与之前修改的file.conf内容不完全一致。
将文件修改名称为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 {
  #transaction service group mapping
  vgroup_mapping.seata_tx_group = "default" #修改自定义事务名称
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    async.commit.buffer.limit = 10000
    lock {
      retry.internal = 10
      retry.times = 30
      retry.policy.branch-rollback-on-conflict = true
    }
    report.retry.count = 5
    table.meta.check.enable = false
    report.success.enable = true
  }
  tm {
    commit.retry.count = 5
    rollback.retry.count = 5
  }
  undo {
    data.validation = true
    log.serialization = "jackson"
    log.table = "undo_log"
  }
  log {
    exceptionRate = 100
  }
  support {
    # auto proxy the DataSource bean
    spring.datasource.autoproxy = false
  }
}

## transaction log store
store {
  ## store mode: file、db
  mode = "file"
  ## file store property
  file {
    ## store location dir
    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 property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://172.16.138.100:3306/seata"  #修改db信息
    user = "root"
    password = "xxxxx"
    min-conn = 1
    max-conn = 10
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
server {
  recovery {
    #schedule committing retry period in milliseconds
    committing-retry-period = 1000
    #schedule asyn committing retry period in milliseconds
    asyn-committing-retry-period = 1000
    #schedule rollbacking retry period in milliseconds
    rollbacking-retry-period = 1000
    #schedule timeout retry period in milliseconds
    timeout-retry-period = 1000
  }
  undo {
    log.save.days = 7
    #schedule delete expired undo_log in milliseconds
    log.delete.period = 86400000
  }
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

修改信息:
image.png
image.png
registry.conf
将seata目录conf下的registry.conf文件拷贝到资源目录中,该文件与之前修改的file.conf内容基本一致。

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    serverAddr = "172.16.138.100:8848"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

修改信息:
image.png

启动报错

seata版本一定要与pom依赖的版本保持一致!!!
Seata控制台报错:
image.png
排除包内自带的seata,重新引入与自己版本一直的依赖包。

      	<!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.0.0</version>
        </dependency>

报错:not support register type: null
image.png
注意SpringCloudAlibaba版本与Seata之间的版本对应关系

            <!--spring-cloud-alibaba-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

版本依赖关系
我用的是1.0.0版本的seata,与之对应的spring-cloud-alibaba版本应该是2.0.0.RELEASE。切换版本
image.png
代理源对象报错:
image.png
seata版本问题:

<!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.0.0</version>
        </dependency>

将版本号从1.0.0改为0.9.0:

    	<!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>

解决:

seata:
  enabled: false #取消自动装配数据源

报错:not support register type: null
image.png
解决:实在没解决办法了。
版本回退,seata改为0.9.0。

      	<!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.0</version>
        </dependency>

file.conf和registry.conf配置同上。
成功启动
image.png
全部微服务启动完成:
image.png

@GlobalTransactional

基础数据
t_account表数据:
image.png
t_order表数据:
image.png
t_storage表数据:
image.png

模拟正常下单

发送post请求:http://localhost:2001/order/createOrder
请求参数:json

{
  "id": null,
  "userId": 1,
  "productId": 1,
  "count": 10,
  "money": "100.0",
  "status": 0
}

t_account表数据:
image.png
t_order表数据:
image.png
t_storage表数据:
image.png
正常下订单没问题,整体流程需要保证分布式事务,加起来需要考虑到高并发JUC的问题。

模拟异常下订单

常见全局事务问题:

  • 库存和账户金额扣减完成后,订单状态未完成修改,还是创建中,而不是创建完成;
  • feign重试机制,账户金额可能会被多次扣减。

模拟超时异常:

import com.xiu.account.mapper.AccountMapper;
import com.xiu.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * @author zhangzengxiu
 * @date 2023/2/23
 */
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public boolean decreaseAccount(Long userId, BigDecimal money) {
        try {
            //模拟业务超时
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int res = accountMapper.decreaseAccount(userId, money);
        return res == 1;
    }
}

发送请求:
image.png
t_order订单表:新建了订单,但是状态仍然未改变为已完成。
image.png
t_account账户表:账户未扣减
image.png
t_storage库存表:库存被扣减
image.png
添加注解:@GlobalTransactional 来控制全局事务

    @GlobalTransactional(name = "seata_tx_group", rollbackFor = Exception.class)
    @Override
    public boolean createOrder(Order order) {
        //创建新订单
        orderMapper.addOrder(order);
        //库存扣减
        storageFeign.decreaseStorage(order.getProductId(), order.getCount());
        //账户扣减
        accountFeign.decreaseAccount(order.getUserId(), order.getMoney());
        //修改订单状态
        orderMapper.updateOrderStatus(order.getId(), OrderStatusEnum.END.getStatusCode());
        return true;
    }

再次发送请求,本次请求并未被三张表造成任何写操作,全局事务控制成功!
0.9版本Seata不支持集群!!!

Seata原理

TC:seata服务器
TM:事务发起方(添加@GlobalTransactional注解的方法)
RM:每个数据库就是一个RM,事务的参与方

执行流程:

  • TM开启分布式事务(TM向TC注册全局分布式事务)
  • 按照业务场景,编排数据库,服务等事务资源(RM向TC汇报资源准备状态)
  • TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务)
  • TC汇总事务消息,决定分布式事务是提交还是回滚
  • TC通知所有RM提交/回滚资源,事务二阶段结束

AT模式

前提
  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

image.png
image.png

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。
一阶段加载

第一阶段,Seata会拦截“业务SQL”
1、解析SQL语义,找到业务SQL要更新的数据,在业务数据被更新前,将其保存成“before image”
2、执行业务SQL,更新业务数据,在业务数据更新之后
3、将其保存成“after image”,最后生成行锁(lock_table会有体现)
以上操作全部在一个数据库的事务中完成,这样保证了一阶段的操作原子性
image.png

二阶段提交

因为业务SQL在第一阶段已经提交到数据库,所以seata框架需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
不加锁了就可以提交事务了。
image.png

二阶段回滚

二阶段如果是回滚操作的话,Seata就需要回滚到一阶段已经执行的业务SQL还原数据。(反向补偿)
回滚方式便是用before image还原业务数据;但是还原前,要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原数据,如果不一致,说明有脏写,需要人工处理。
image.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值