随着业务的不断发展,单体架构已经无法满足我们的需求,分布式微服务架构逐渐成为大型互联网平台的首选,但所有使用分布式微服务架构的应用都必须面临一个十分棘手的问题,那就是“分布式事务”问题。
在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于其中的某个服务而言,它的数据一致性可以交由其自身数据库事务来保证,但从整个分布式微服务架构来看,其全局数据的一致性却是无法保证的。
用户购买商品的业务逻辑。整个业务需要调用三个微服务:
为了保证数据的正确性和一致性,我们必须保证所有这些操作要么全部成功,要么全部失败,否则就可能出现类似于商品库存已扣减,但用户账户资金尚未扣减的情况。各服务自身的事务特性显然是无法实现这一目标的,此时,我们可以通过分布式事务框架来解决这个问题。
Seata 就是这样一个分布式事务处理框架,它是由阿里巴巴和蚂蚁金服共同开源的分布式事务解决方案,能够在微服务架构下提供高性能且简单易用的分布式事务服务。
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
一个典型的分布式事务过程:主要是通过 XID 和 3 个核心组件实现的。
以上三个组件相互协作,TC 以 Seata 服务器(Server)形式独立部署,TM 和 RM 则是以 Seata Client 的形式集成在微服务中运行,其整体工作流程如下图。
处理过程:
Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。
在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。
任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:
此外,我们还需要针对业务中涉及的各个数据库表,分别创建一个 UNDO_LOG(回滚日志)表。
Seata 的 AT 模式工作时大致可以分为以两个阶段,下面我们就结合一个实例来对 AT 模式的工作机制进行介绍。
假设某数据库中存在一张名为 webset 的表,表结构如下。
在某次分支事务中,我们需要在 webset 表中执行以下操作。
update webset set url = 'c.biancheng.net' where name = 'C语言中文网';
一阶段
Seata AT 模式一阶段的工作流程如下图所示。
{
"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
"xid": "172.26.54.1:8091:5962967415319516023",
"branchId": 5962967415319516027,
"sqlUndoLogs": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
"sqlType": "UPDATE",
"tableName": "webset",
"beforeImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "webset",
"rows": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.sql.struct.Row",
"fields": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": -5,
"value": [
"java.lang.Long",
1
]
},
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "url",
"keyType": "NULL",
"type": 12,
"value": "biancheng.net"
}
]
]
}
]
]
},
"afterImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "webset",
"rows": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.sql.struct.Row",
"fields": [
"java.util.ArrayList",
[
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": -5,
"value": [
"java.lang.Long",
1
]
},
{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "url",
"keyType": "NULL",
"type": 12,
"value": "c.biancheng.net"
}
]
]
}
]
]
}
}
]
]
}
以上所有操作均在同一个数据库事务内完成,可以保证一阶段的操作的原子性。
二阶段:提交
当所有的 RM 都将自己分支事务的提交结果上报给 TC 后,TM 根据 TC 收集的各个分支事务的执行结果,来决定向 TC 发起全局事务的提交或回滚。
若所有分支事务都执行成功,TM 向 TC 发起全局事务的提交,并批量删除各个 RM 保存的 UNDO_LOG 记录和行锁;否则全局事务回滚。
二阶段:回滚
若全局事务中的任何一个分支事务失败,则 TM 向 TC 发起全局事务的回滚,并开启一个本地事务,执行如下操作。
https://github.com/seata/seata/releases
我下载的是1.4.2版本。同时也要下载源码包seata-1.4.2.zip(DB模式下要执行SQL脚本以及Nacos作为配置中心,Seata相关的配置需要注册到Nacos
Server端存储模式(store.mode)现有file、db、redis三种(后续将引入raft,mongodb),file模式无需改动,直接启动即可,下面专门讲下db启动步骤。
file模式为单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高;
db模式为高可用模式,全局事务会话信息通过db共享,相应性能差些;
redis模式Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置.
目录是:
seata-server-1.4.2\seata\conf
需要修改的配置文件
registry.conf
file.conf
registry.conf指定注册模式,如 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa,默认:type = “file”,指向file.conf文件
file.conf:有模式:file、db、redis,用于存储事务的日记。
registry.conf当前指定使用nacos作为注册中心和配置中心,需要修改registry项和config项,同时修改nacos的连接信息。
建议增加namespace的配置,将Seata相应的配置放在一起,不然后面配置中心的东西很多
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "localhost:8848"
group = "SEATA_GROUP"
namespace = "61b373e2-b229-46e4-925d-5220f8653886"
cluster = "default"
username = "nacos"
password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
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 = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "61b373e2-b229-46e4-925d-5220f8653886"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
consul {
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
apollo {
appId = "seata-server"
## apolloConfigService will cover apolloMeta
apolloMeta = "http://192.168.1.204:8801"
apolloConfigService = "http://192.168.1.204:8080"
namespace = "application"
apolloAccesskeySecret = ""
cluster = "seata"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
nodePath = "/seata/seata.properties"
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
file.conf
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## rsa decryption public key
publicKey = ""
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "dbcp"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://localhost:3306/seata?serverTimezone=GMT%2B8&characterEncoding=utf8&connectTimeout=10000&socketTimeout=30000&autoReconnect=true&useSSL=false"
user = "root"
password = "root"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
## redis mode: single、sentinel
mode = "single"
## single mode property
single {
host = "127.0.0.1"
port = "6379"
}
## sentinel mode property
sentinel {
masterName = ""
## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
sentinelHosts = ""
}
password = ""
database = "0"
minConn = 1
maxConn = 10
maxTotal = 100
queryLimit = 100
}
}
Server端:server端数据库脚本 (包含 lock_table、branch_table 与 global_table) 及各个容器配置,脚本需要在源码包找(seata-1.4.2.zip)
执行脚本(仅使用DB模式才需要执行脚本)
脚本所在的目录(seata-1.4.2为源码seata-1.4.2.zip)找见mysql.sql
seata-1.4.2\script\server\db
-- -------------------------------- 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(6),
`gmt_modified` DATETIME(6),
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(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;
启动Seata,记得先启动Nacos
Windows启动:
进入目录:seata-server-1.4.2\bin,双击seata-server.bat直接启动
Linux命令启动:
seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
参数说明:
-h: 注册到注册中心的ip
-p: Server rpc 监听端口
-m: 全局事务会话信息存储模式,file、db、redis,优先读取启动参数 (Seata-Server 1.3及以上版本支持redis)
-n: Server node,多个Server时,需区分各自节点,用于生成不同区间的transactionId,以免冲突
-e: 多环境配置参考 http://seata.io/en-us/docs/ops/multi-configuration-isolation.html
config-center 官网说明地址是:
https://github.com/seata/seata/tree/develop/script/config-center
该文件在源码包中:seata-1.4.2.zip,解压后的目录是:
seata-1.4.2\script\config-center\config.txt
当前使用的是Nacos作为配置中心(config-center),所以进入到nacos目录:
seata-1.4.2\script\config-center\nacos
有两个脚本文件:
nacos-config.py
nacos-config.sh
第一个是phython脚本,第二个是Linux脚本
那Windows脚本怎么执行呢?安装Git-2.26.2-64-bit.exe,使用Git Bash执行。
my_test_tx_group是默认有的,没有修改。增加分布式事务分组nacos_producer_tx_group、nacos_consumer_tx_group。注意:名称长度不能超过VARCHAR(32)。事务分组允许只配置一个,也可以配置成多个,用于切换,此处额外增加2个,具体配置如下:
service.vgroupMapping.my_test_tx_group=default
service.vgroupMapping.nacos-service-tx-group=default
- vgroup_mapping.my-tx-group = "seata-server"为事务组名称,这里的值需要和TC中配置的service.vgroup-mapping.my-tx-group一致;
- 事务组的命名不要用下划线’_‘,可以用’-'因为在seata的高版本中使用underline下划线 将导致service not to be found。
什么是事务分组?见:
https://seata.io/zh-cn/docs/user/txgroup/transaction-group.html
修改数据存储模式为db模式,修改数据连接和账号密码
store.mode=db
store.db.datasource=dbcp
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
建议增加namespace的配置(-t参数,命名空间的id是在Nacos创建生成的),将Seata相应的配置放在一起
通过Git Bash将相应的配置导致到Nacos配置中心,命令如下
sh D:/java/seata/seata-server-1.4.2/config-center/nacos/nacos-config.sh -h 127.0.0.1 -p 8848 -g SEATA_GRUOP -t 61b373e2-b229-46e4-925d-5220f8653886 -u nacos -w nacos
config.txt文件脚本执行后,成功91,失败4个,但失败的不影响Seata运行。
不导入config.txt文件,项目加入Seata依赖和配置启动后,会出现下面的错误:can not get cluster name in registry config,详细如下:
SPRING-CLOUD-NACOS-SERVICE服务提供者:
2021-05-10 09:27:18.719 ERROR 13544 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.SPRING-CLOUD-NACOS-SERVICE_tx_group', please make sure registry config correct
2021-05-10 09:27:18.753 ERROR 13544 --- [eoutChecker_2_1] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.SPRING-CLOUD-NACOS-SERVICE_tx_group', please make sure registry config correct
SPRING-CLOUD-NACOS-CONSUMER服务消费者:
2021-05-10 09:27:33.786 ERROR [SPRING-CLOUD-NACOS-CONSUMER,,,] 14916 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.SPRING-CLOUD-NACOS-CONSUMER_tx_group', please make sure registry config correct
2021-05-10 09:27:33.820 ERROR [SPRING-CLOUD-NACOS-CONSUMER,,,] 14916 --- [eoutChecker_2_1] i.s.c.r.netty.NettyClientChannelManager : can not get cluster name in registry config 'service.vgroupMapping.SPRING-CLOUD-NACOS-CONSUMER_tx_group', please make sure registry config correct
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
seata_order:存储订单的数据库;
seata_storage:存储库存的数据库;
seata_account:存储账户信息的数据库。
create database seata_order;
create database seata_storage;
create database seata_account;
seata_order库下建t_order表
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) 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:已完结',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;
seata_storage库下建t_storage 表
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`total` int(11) DEFAULT NULL COMMENT '总库存',
`used` int(11) DEFAULT NULL COMMENT '已用库存',
`residue` int(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
seata_account库下建t_account 表
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL COMMENT 'id',
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`total` decimal(10, 0) DEFAULT NULL COMMENT '总额度',
`used` decimal(10, 0) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10, 0) DEFAULT NULL COMMENT '剩余可用额度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账户表' ROW_FORMAT = Dynamic;
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
3个库下都需要建各自的回滚日志表
脚本在源码包,目录是:
seata-1.4.2\script\client\at\db
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `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(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
业务需求 下订单->减库存->扣余额->改订单状态
Seata依赖说明(建议单选)
依赖seata-all
依赖seata-spring-boot-starter,支持yml、properties配置(.conf可删除),内部已依赖seata-all
依赖spring-cloud-alibaba-seata,内部集成了seata,并实现了xid传递
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>1.4.2version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.6version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.0.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
# 自定义事务组名称需要与seata-server中的对应
tx-service-group: nacos-service-tx-group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: root
#关闭feign的hystrix支持
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
拷贝seata-server/conf目录下的file.conf、registry.conf复制到resourses目录下面
Order
package com.atguigu.springcloud.domain;
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; //订单状态:0:创建中;1:已完结
}
CommonResult
package com.atguigu.springcloud.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this.code = code;
this.message = message;
}
}
package com.atguigu.springcloud.dao;
import com.atguigu.springcloud.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* @Author cjz
* @Date 2022/3/29 21:54
*/
@Mapper
public interface OrderDao {
//1 新建订单
void create(Order order);
//2 修改订单状态,从零改为1
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.springcloud.dao.OrderDao">
<resultMap id="order" type="com.atguigu.springcloud.domain.Order">
<result property="id" column="id" jdbcType="BIGINT"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="productId" column="product_id" jdbcType="BIGINT"/>
<result property="count" column="count" jdbcType="INTEGER"/>
<result property="money" column="money" jdbcType="BIGINT"/>
<result property="status" column="status" jdbcType="INTEGER"/>
resultMap>
<insert id="create">
insert into t_order(user_id, product_id, count, money, status)
value (#{userId},#{productId},#{count},#{money},0)
insert>
<update id="update">
update t_order
set status = 1
where user_id = #{userId}
and status = #{status}
update>
mapper>
AccountService
package com.atguigu.springcloud.service;
import com.atguigu.springcloud.domain.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;
@FeignClient(value = "seata-account-service")
public interface AccountService {
@PostMapping("/account/decrease")
CommonResult decrease(@RequestParam("userId") Long productId, @RequestParam("money") BigDecimal money);
}
StorageService
package com.atguigu.springcloud.service;
import com.atguigu.springcloud.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "seata-storage-service")
public interface StorageService {
@PostMapping("/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
OrderService
package com.atguigu.springcloud.service;
import com.atguigu.springcloud.domain.Order;
public interface OrderService {
void create(Order order);
}
OrderServiceImpl
package com.atguigu.springcloud.service.impl;
import com.atguigu.springcloud.dao.OrderDao;
import com.atguigu.springcloud.domain.Order;
import com.atguigu.springcloud.service.AccountService;
import com.atguigu.springcloud.service.OrderService;
import com.atguigu.springcloud.service.StorageService;
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 OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户扣减账户->修改订单状态
* @param order
*/
@Override
//@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
log.info("------------>开始新建订单");
orderDao.create(order);
log.info("------------>订单微服务开始调用库存,做扣减*****start");
storageService.decrease(order.getProductId(),order.getCount());
log.info("------------>订单微服务开始调用库存,做扣减*****end");
log.info("------------>订单微服务开始调用账户,做扣减*****start");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("------------>订单微服务开始调用账户,做扣减*****end");
//修改订单状态,从零到1 1 代表已经完成
log.info("------------>修改订单状态,*****start");
orderDao.update(order.getUserId(),0);
log.info("------------>修改订单状态,*****end");
log.info("------------>下订单结束,哈哈哈哈哈哈哈");
}
}
整体业务事调用库存服务 扣减库存->调用账户扣减账户->修改订单状态 我们暂时将@GlobalTransactional注释显示分布式事务
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.domain.CommonResult;
import com.atguigu.springcloud.domain.Order;
import com.atguigu.springcloud.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order){
orderService.create(order);
return new CommonResult<>(200,"订单创建成功");
}
}
DataSourceProxyConfig:
package com.atguigu.springcloud.config;
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.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 使用Seata对数据源进行代理
*/
@Configuration
public class DataSourceSeataProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
package com.atguigu.springcloud;
import org.mybatis.spring.annotation.MapperScan;
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;
/**
* exclude = DataSourceAutoConfiguration.class 取消数据源的自动创建,
* 读取自定义的DataSourceProxyConfig.class类,使用Seata对数据源进行代理
*/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan(value = "com.atguigu.springcloud.dao")
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
具体详情就不写了,可以取gitee仓库地址查看
gitee仓库地址
具体详情就不写不出来了,可以取gitee仓库地址查看
gitee仓库地址
下订单->减库存->扣余额->改(订单)状态**
启动三个微服务
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
数据库情况
可以看到,正常下单是没有问题的,库存减10,账户月减少100,订单状态变为1
AccountServiceImpl
由于20秒已经超过了OpenFeign的超时时间,所以会执行失败。
再访问:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
数据库情况
没下单成功竟然减掉了库存和用户的账户余额。
AccountServiceImpl
OrderServiceImpl添加@GlobalTransactional注解,这个注解的作用是只要发生异常就回滚
再次访问:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
可以看到,下单后数据库数据并没有任何改变,数据都没有添加进来,分布式事务回滚测试是成功的。
在分布式微服务架构中,我们可以使用 Seata 提供的 @GlobalTransactional 注解实现分布式事务的开启、管理和控制。
当调用 @GlobalTransaction 注解的方法时,TM 会先向 TC 注册全局事务,TC 生成一个全局唯一的 XID,返回给 TM。
@GlobalTransactional 注解既可以在类上使用,也可以在类方法上使用,该注解的使用位置决定了全局事务的范围,具体关系如下:
处理过程: