Seata 概念
业务的不断发展,单体架构无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的首选,所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是“分布式事务”问题。在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身 数据库事务 来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。 如:用户在某电商系统下单购买了一件商品后,电商系统会执行下 4 步:- 调用订单服务创建订单数据
- 调用库存服务扣减库存
- 调用账户服务扣减账户金额
- 最后调用订单服务修改订单状态
- TC(Transaction Coordinator):事务协调器,它是事务的协调者(这里指的是 Seata 服务器),主要负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
- TM(Transaction Manager):事务管理器,它是事务的发起者,负责定义全局事务的范围,并根据 TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
- RM(Resource Manager):资源管理器,它是资源的管理者(这里可以将其理解为各服务使用的数据库)。它负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
- TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局唯一的 XID;
- XID 通过服务的调用链传递到其他服务;
- RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖;
- TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
- TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作。
Seata下载安装
解压后,目录如下
:::color1
目录说明如下:<font style="color:rgb(68, 68, 68);">bin</font>
:用于存放<font style="color:rgb(68, 68, 68);">Seata Server</font>
可执行命令。<font style="color:rgb(68, 68, 68);">conf</font>
:用于存放<font style="color:rgb(68, 68, 68);">Seata Server</font>
的配置文件。<font style="color:rgb(68, 68, 68);">lib</font>
:用于存放<font style="color:rgb(68, 68, 68);">Seata Server</font>
依赖的各种 Jar 包。<font style="color:rgb(68, 68, 68);">logs</font>
:用于存放<font style="color:rgb(68, 68, 68);">Seata Server</font>
的日志。
:::
Seata 事务模式
Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。详细可参考官网在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。
Seata AT
要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:- 须使用支持本地 ACID 事务特性的关系型数据库,例如
<font style="color:rgb(68, 68, 68);">MySQL</font>
、<font style="color:rgb(68, 68, 68);">Oracle </font>
等 - 应用程序必须是使用 JDBC 对数据库进行访问的 JAVA 应用
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
Seata 配置中心
Seata 配置中心 所谓“配置中心”,就像是一个“大衣柜”,内部存放着各种各样的配置文件,我们可以根据自己的需要从其中获取指定的配置文件,加载到对应的客户端中。详细参考官网Seata 支持多种配置中心:
- nacos
- consul
- apollo
- etcd
- zookeeper
- file (读本地文件,包含 conf、properties、yml 等配置文件)
Seata 整合Nacos配置中心
对于 Seata 来说,Nacos 是一种重要的配置中心实现。官网参考:Seata部署指南,使用步骤如下:添加Maven依赖
在 Spring Cloud 项目中使用Seata,通常只需要在<font style="color:rgb(68, 68, 68);">pom.xml</font>
中添加 <font style="color:rgb(68, 68, 68);">spring-cloud-starter-alibaba-seata</font>
依赖即可
<!--引入 seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
Seata Server 配置
在 Seata Server 安装目录下的<font style="color:rgb(68, 68, 68);">config/registry.conf</font>
中,将配置方式<font style="color:rgb(68, 68, 68);">config.type</font>
修改为 Nacos,并对 Nacos 配置中心的相关信息进行配置,示例配置如下(默认的是用文件(file)配置,本来里面放了很多选项,既然我们用nacos,其他选项就可以删掉)。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa(seata支持多种配置中心,我们主要使用nacos)
type = "nacos"
nacos {
application = "seata-server"
#修改为使用的 nacos 服务器地址
serverAddr = "127.0.0.1:8848"
#配置中心所在的分组
group = "SEATA_GROUP"
#配置中心的命名空间
namespace = ""
cluster = "default"
#Nacos 配置中心的用户名
username = "nacos"
#Nacos 配置中心的密码
password = "nacos"
}
}
Seata Client 配置
在 Seata Client(即微服务架构中的服务,就是项目中的配置),通过<font style="color:rgb(68, 68, 68);">application.yml</font>
等配置文件对 Nacos 配置中心进行配置,示例代码如下。
server:
port: 8501
spring:
application:
name: chen-seata-server
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# seata配置文件
seata:
registry:
type: nacos
nacos:
application: chen-seata-server
server-addr: 127.0.0.1:8848 # Nacos 注册中心的地址
group: "SEATA_GROUP" #分组
namespace: ""
username: nacos
password: nacos
@SpringBootApplication
@EnableDiscoveryClient//开启服务注册与发现功能(不要忘记哦)
public class SpringCloudAlibabaSeata8501Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaSeata8501Application.class, args);
}
}
完成以上步骤,先启动 Nacos Server 再启动 Seata Server,登录 Nacos 控制台查看服务列表如下:
<font style="color:rgb(68, 68, 68);">chen-seata-server</font>
服务已经注册到了 Nacos 注册中心。如果运行报错,请检查是否导入Nacos依赖,或者Nacos配置
上传配置到 Nacos 配置中心
在完成了 Seata 服务端和客户端的相关配置后,就可以将配置上传到Nacos 配置中心了,操作步骤如下。
- 我们需要获取一个名为
config.txt
的文本文件,该文件包含了 Seata 配置的所有参数明细。去官网下载源码里面查找
- 在
/script/config-center/nacos
目录中,有以下 2 个 Seata 脚本:
nacos-config.py
:Python 脚本nacos-config.sh
:为 Linux 脚本,可以在 Windows 下通过 Git 命令,将 config.txt 中的 Seata 配置上传到 Nacos 配置中心
seata-1.4.2\script\config-center\nacos
目录下,右键鼠标选择Git Bush Here
,并在弹出的 Git 命令窗口中执行以下命令,将config.txt
中的配置上传到 Nacos 配置中心。注意:上传时Nacos服务一定要启动
sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -u nacos -w nacos
Git 命令各参数说明:
- -h:Nacos 的 host,默认取值为 localhost
- -p:端口号,默认取值为
<font style="color:rgb(68, 68, 68);">8848</font>
- -g:Nacos 配置的分组,默认取值为
<font style="color:rgb(68, 68, 68);">SEATA_GROUP</font>
- -u:Nacos 用户名
- -w:Nacos 密码
看到如上图所示代表上传成功,可以到Nacos配置中心验证是否上传成功
Seata事务分组(待完善……)
事务分组是 Seata 提供的一种 TC(Seata Server) 服务查找机制。Seata 通过事务分组获取 TC 服务,流程如下:- 在应用中配置事务分组。
- 应用通过配置中心去查找配置:service.vgroupMapping.{事务分组},该配置的值就是 TC 集群的名称。
- 获得集群名称后,应用通过一定的前后缀 + 集群名称去构造服务名。
- 得到服务名后,去注册中心去拉取服务列表,获得后端真实的 TC 服务列表。
Seata Server配置
在 Seata Server 的 config/registry.conf 中,进行如下配置。registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" #使用 Nacos作为注册中心
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos" # 使用nacos作为配置中心
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
Seata Cline配置
seata:
registry:
type: nacos #从 Nacos 获取 TC 服务
nacos:
application: chen-seata-server
server-addr: 127.0.0.1:8848 # Nacos 注册中心的地址
group: "SEATA_GROUP"
namespace: ""
username: nacos
password: nacos
config:
type: nacos #使用 Nacos 作为配置中心
nacos:
application: chen-seata-server
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
namespace: ""
tx-service-group: service-order-group
启动Seata Server
建表
Seata Server 目前有以下 3 种存储模式(store.mode)(官网后续标明将引入<font style="color:rgb(36, 41, 46);">raft</font>
,<font style="color:rgb(36, 41, 46);">mongodb</font>
):详细参考官网Seata部署指南
模式 | 说明 | 准备工作 |
---|---|---|
file | 文件存储模式:默认存储模式; 该模式为单机模式,全局事务的会话信息在内存中读写,并持久化本地文件 <font style="color:rgb(51, 51, 51);">root.data</font> ,性能较高 |
* |
db | 数据库存储模式:该模式为高可用模式,全局事务会话信息通过数据库共享,相应性能差些 | 建数据库表 |
redis | 缓存处处模式: Seata Server 1.3 及以上版本支持该模式,性能较高,但存在事务信息丢失风险 | 配置 redis 持久化配置 |
- 全局事务,对应的表为:
<font style="color:rgb(68, 68, 68);">global_table</font>
- 分支事务,对应的表为:
<font style="color:rgb(68, 68, 68);">branch_table</font>
- 全局锁,对应的表为:
<font style="color:rgb(68, 68, 68);">lock_table</font>
<font style="color:rgb(68, 68, 68);">seata</font>
的数据库实例,并在该数据库内执行以下 SQL
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;
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(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`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;
上方SQL可以在相应版本的源码包里面的 seata-1.4.2\script\server\db
路径下面找到
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "chen-seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
修改配置文件 <font style="color:rgb(68, 68, 68);">config.txt</font>
中的数据库连接
#将 Seata Server 的存储模式修改为 db
store.mode=db
# 数据库驱动
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 数据库 url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&characterEncoding=UTF-8&useUnicode=true&serverTimezone=UTC
# 数据库的用户名
store.db.user=root
# 数据库的密码
store.db.password=cj123456789
# 自定义事务分组
service.vgroupMapping.service-order-group=default
service.vgroupMapping.service-storage-group=default
service.vgroupMapping.service-account-group=default
在 seata-1.4.2\script\config-center\nacos 目录下,右键鼠标选择 Git Bush Here,在弹出的 Git 命令窗口中执行以下命令,将 config.txt 中的配置上传到 Nacos 配置中心。
sh nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GROUP -u nacos -w nacos
当 Git 命令窗口出现以下执行日志时,则说明配置上传成功。

- Order(订单服务):创建和修改订单;
- Storage(库存服务):对指定的商品扣除仓库库存;
- Account(账户服务) :从用户帐户中扣除商品金额。
当用户从平台购买一件商品时,触发的步骤如下:
- 调用订单服务Order,创建一条订单记录,状态未完成
- 调用库存服务Storage,执行库存的扣减
- 调用账户服务Account,从当前账户扣减相应产品金额
- 上述步骤完成后调用订单服务Order,调整订单状态为已完成
创建订单服务
建表
在 MySQL 数据库中,新建一个名为 seata-order
的数据库实例,并通过以下 SQL 语句创建 2 张表:order(订单表)和 undo_log(回滚日志表)。
-- 创建订单服务数据库
CREATE DATABASE IF NOT EXISTS seata_order
DROP TABLE IF EXISTS `s_order`;
CREATE TABLE `s_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint DEFAULT NULL COMMENT '用户id',
`product_id` bigint DEFAULT NULL COMMENT '产品id',
`count` int DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
`status` int DEFAULT NULL COMMENT '订单状态:0:未完成;1:已完结',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int 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 KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
创建Module spring-cloud-alibaba-seata-order-8005
添加依赖 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--父工程-->
<parent>
<groupId>com.chen</groupId>
<artifactId>spring-cloud-alibaba-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.chen</groupId>
<artifactId>spring-cloud-alibaba-seata-order-8005</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-alibaba-seata-order-8005</name>
<description>spring-cloud-alibaba-seata-order-8005</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入 seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</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-nacos-config</artifactId>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--引入MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yaml
server:
port: 8005 #端口
spring:
application:
name: spring-cloud-alibaba-seata-order-8005 #服务吗
#数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动,如果是8.0以下使用 com.mysql.jdbc.Driver
name: defaultDataSource
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC #???????
username: root #账号
password: cj123456789 #密码
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos连接地址
namespace: public #nacos命名空间
username: nacos
password: nacos
sentinel:
transport:
dashboard: 127.0.0.1:8080 #Sentinel地址
port: 8719
alibaba:
seata:
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-order-group
seata:
application-id: ${spring.application.name}
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-order-group
service:
grouplist:
#Seata 服务器地址
seata-server: 127.0.0.1:8091
# Seata 的注册方式为 nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
# Seata 的配置中心为 nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
feign:
sentinel:
enabled: true #开启OpenFeign功能
management:
endpoints:
web:
exposure:
include: "*"
mybatis:
mapperLocations: classpath:mybatis/*.xml
entity、mapper、service、controller、vo
entity
package com.chen.springcloudalibabaseataorder8005.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status;
}
mapper
package com.chen.springcloudalibabaseataorder8005.mapper;
import com.chen.springcloudalibabaseataorder8005.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderMapper {
/**
* 创建订单
*/
int create(Order order);
/**
* 修改订单状态,从零改为1
*/
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
mapper.xml
注意:mapper文件我这里放在当前项目\src\main\resources\mybatis\
下面。可以根据根据自己喜好放置,记得一定要做相应设置不然无法映射。我这里在 文件中配置的 mybatis-mapperLocations
<?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.chen.springcloudalibabaseataorder8005.mapper.OrderMapper">
<!--定义一个结果集和实体类的映射表-->
<resultMap id="BaseResultMap" type="com.chen.springcloudalibabaseataorder8005.entity.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create">
INSERT INTO `order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`)
VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, #{status});
</insert>
<update id="update">
UPDATE `order`
SET status = #{status}
WHERE user_id = #{userId} AND status = #{status};
</update>
</mapper>
service
订单服务下面的Service要创建三个。一个是订单自己的Service。 加调用库存和账户两个。
package com.chen.springcloudalibabaseataorder8005.service;
import com.chen.springcloudalibabaseataorder8005.entity.Order;
import com.chen.springcloudalibabaseataorder8005.vo.CommonResult;
public interface OrderService {
/**
* 创建订单数据
*/
CommonResult create(Order order);
}
package com.chen.springcloudalibabaseataorder8005.service;
import com.chen.springcloudalibabaseataorder8005.vo.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
//通过OpenFeign远程调用库存的微服务
@FeignClient(value = "spring-cloud-alibaba-seata-storage-8006")
public interface StorageService {
/**
* 扣减库存
*/
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
package com.chen.springcloudalibabaseataorder8005.service;
import com.chen.springcloudalibabaseataorder8005.vo.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;
//通过OpenFeign远程调用账号微服务
@FeignClient(value = "spring-cloud-alibaba-seata-account-8007")
public interface AccountService {
/**
* 扣减账户余额,需要传入用户ID跟扣除的金额
*/
@PostMapping("/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
serviceImpl
package com.chen.springcloudalibabaseataorder8005.service.impl;
import com.chen.springcloudalibabaseataorder8005.entity.Order;
import com.chen.springcloudalibabaseataorder8005.mapper.OrderMapper;
import com.chen.springcloudalibabaseataorder8005.service.AccountService;
import com.chen.springcloudalibabaseataorder8005.service.OrderService;
import com.chen.springcloudalibabaseataorder8005.service.StorageService;
import com.chen.springcloudalibabaseataorder8005.vo.CommonResult;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 1.创建订单->2.调用库存服务扣减库存->3.调用账户服务扣减账户余额->4.修改订单状态
*/
@GlobalTransactional
@Override
public CommonResult create(Order order) {
log.info("------->下单开始");
//本应用创建订单
orderMapper.create(order);
//远程调用库存服务扣减库存
log.info("------->订单微服务调用库存微服务,扣减库存开始");
storageService.decrease(order.getProductId(), order.getCount());
log.info("------->订单微服务调用库存微服务,扣减库存结束");
//远程调用账户服务扣减余额
log.info("------->订单微服务调用账户微服务,扣减余额开始");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("------->订单微服务调用账户微服务,减余额结束");
//修改订单状态为已完成
log.info("------->order-service中修改订单状态开始");
// 这里的话是不是应该是orderId
orderMapper.update(order.getUserId(), 0);
log.info("------->order-service中修改订单状态结束");
return new CommonResult(200,"订单创建成功");
}
}
vo
package com.chen.springcloudalibabaseataorder8005.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult implements Serializable {
private static final long serialVersionUID = 1L;
private int code;
private String msg;
private Object data;
public CommonResult(int code,String msg){
this(code,msg,null);
}
}
controller
package com.chen.springcloudalibabaseataorder8005.controller;
import com.chen.springcloudalibabaseataorder8005.entity.Order;
import com.chen.springcloudalibabaseataorder8005.service.OrderService;
import com.chen.springcloudalibabaseataorder8005.vo.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/order/create/{productId}/{count}/{money}")
public CommonResult create(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count, @PathVariable("money") BigDecimal money) {
Order order = new Order();
order.setUserId(1L);
order.setProductId(Integer.valueOf(productId).longValue());
order.setCount(count);
order.setMoney(money);
order.setStatus(0);
return orderService.create(order);
}
}
搭建库存服务
建表
在 MySQL 数据库中,新建一个名为 seata_storage
的数据库实例,并通过以下 SQL 语句创建 2 张表:storage(订单表)和 undo_log(回滚日志表)。
-- 创建库存服务数据库
CREATE DATABASE IF NOT EXISTS seata_storage;
DROP TABLE IF EXISTS `storage`;
CREATE TABLE `storage` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint DEFAULT NULL COMMENT '产品id',
`total` int DEFAULT NULL COMMENT '总库存',
`used` int DEFAULT NULL COMMENT '已用库存',
`residue` int DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `storage` VALUES ('1', '1', '100', '0', '100');
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int 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 KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';
创建Module spring-cloud-alibaba-seata-storage-8006
添加依赖 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--父工程-->
<parent>
<groupId>com.chen</groupId>
<artifactId>spring-cloud-alibaba-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.chen</groupId>
<artifactId>spring-cloud-alibaba-seata-storage-8006</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-alibaba-seata-storage-8006</name>
<description>spring-cloud-alibaba-seata-storage-8006</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入 seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</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-nacos-config</artifactId>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--引入MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yaml
server:
port: 8006 #端口
spring:
application:
name: spring-cloud-alibaba-seata-storage-8006 #服务名
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动,数据库版本如果是8.0以下的请使用 com.mysql.jdbc.Driver
name: defaultDataSource
url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC #数据库连接地址
username: root #数据库的用户名
password: cj123456789 #数据库密码
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos 服务器地址
namespace: public #nacos 命名空间
username: nacos
password: nacos
sentinel:
transport:
dashboard: 127.0.0.1:8080 #Sentinel 控制台地址
port: 8719
alibaba:
seata:
#自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应,vgroup_mapping.my_test_tx_group = "my_group"
tx-service-group: service-storage-group
seata:
application-id: ${spring.application.name}
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-storage-group
service:
grouplist:
#Seata 服务器地址
seata-server: 127.0.0.1:8091
# Seata 的注册方式为 nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
# Seata 的配置中心为 nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
feign:
sentinel:
enabled: true #开启OpenFeign功能
management:
endpoints:
web:
exposure:
include: "*"
mybatis:
mapperLocations: classpath:mybatis/*.xml
entity、mapper、service、controller、vo
同上方类似,详细代码请访问git,查看获取
spring-cloud-alibaba-seata-storage-8006
搭建账户服务
建表
- 在 MySQL 数据库中,新建一个名为
seata_account
的数据库实例,并通过以下 SQL 语句创建 2 张表:account(订单表)和 undo_log(回滚日志表)。
-- 创建账户服务数据库
CREATE DATABASE IF NOT EXISTS seata_account;
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint DEFAULT NULL COMMENT '用户id',
`total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
`used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `account` VALUES ('1', '1', '1000', '0', '1000');
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int 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 KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建Module spring-cloud-alibaba-seata-account-8007
添加依赖 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--父工程-->
<parent>
<groupId>com.chen</groupId>
<artifactId>spring-cloud-alibaba-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.chen</groupId>
<artifactId>spring-cloud-alibaba-seata-account-8007</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-alibaba-seata-account-8007</name>
<description>spring-cloud-alibaba-seata-account-8007</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入 seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</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-nacos-config</artifactId>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--引入MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yaml
server:
port: 8007 #端口
spring:
application:
name: spring-cloud-alibaba-seata-account-8007 #服务名
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动,数据库版本如果是8.0以下的请使用 com.mysql.jdbc.Driver
name: defaultDataSource
url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC #数据库连接地址
username: root #数据库的用户名
password: cj123456789 #数据库密码
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 #nacos 服务器地址
namespace: public #nacos 命名空间
username: nacos
password: nacos
sentinel:
transport:
dashboard: 127.0.0.1:8080 #Sentinel 控制台地址
port: 8719
alibaba:
seata:
#自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应,vgroup_mapping.my_test_tx_group = "my_group"
tx-service-group: service-account-group
seata:
application-id: ${spring.application.name}
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-account-group
# Seata 的注册方式为 nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
# Seata 的配置中心为 nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
feign:
sentinel:
enabled: true #开启OpenFeign功能
management:
endpoints:
web:
exposure:
include: "*"
mybatis:
mapperLocations: classpath:mybatis/*.xml
entity、mapper、service、controller、vo
同上方类似,详细代码请访问git,查看获取
demo/spring-cloud-alibaba-seata-account-8007
测试
三个服务搭建完毕后,我们进行测试。测试思路如下:
- 不适用分布式事务,看下存在的问题
请求 http://localhost:8005/order/create/1/1/10,模拟创建订单操作,我们在本地想要查看的地方打好断点,一步步执行。
我们在创建完订单扣库存的时候,模拟库粗不足异常。此时去查看表中数据如下:
order
account
storage
会发现,订单创建OK的,但是库存数据和账户金额数据并没有发生变化。导致数据差异产生脏数据
- 开启分布式事务,查看问题是否解决
在创建订单服务添加 <font style="color:#9e880d;">@GlobalTransactional</font>
,开启全局事务。
主配置类上添加开启 <font style="color:#9e880d;">@EnableAutoDataSourceProxy</font>
重复上方测试步骤,当触发异常后,所有服务操作的数据都回退到修改之前的状态。此时全局事务生效控制了数据的ACID特性。
常见错误
truncation: Data too long for column 'application_id' at row 1
io.seata.core.exception.TmTransactionException: TransactionException[begin global request failed. xid=null, msg=Data truncation: Data too long for column 'application_id' at row 1]
此时只需要把 application_id
字段调大即可解决