- Seata分布式事务
- !/usr/bin/env bash
- Copyright 1999-2019 Seata.io Group.
- Licensed under the Apache License, Version 2.0 (the “License”);
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at、
- http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an “AS IS” BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- mybatis
- 实体扫描,多个package用逗号或者分号分隔
Seata分布式事务
传统2PC的问题在seata中得到解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务。
是工作在应用层的中间件。主要的优点是性能较好,且长时间不占用连接资源,它以高效并对业务0侵入的方式解决微服务场景下面临的分布式事务问题。目前提供AT模式(即2pc)及TCC模式的分布式事务解决方案。
Seata的设计思想
把事务的信息以日志的方式记录下来。这种处理方式,实际上是对传统两阶段提交的一种改进和优化:
- 传统两阶段提交协议是阻塞协议,性能差
- 传统两阶段提交协议高可用性不好
- 传统两阶段提交协议的全局事务隔离机制不支持
- 根据八二原则,80%的涉及到全局事务的业务是能正常完成并提交的。
因此,seata采取的做法是,一个事务分支的数据库操作执行完成后,马上进行本地事务的提交,从而释放相关的数据库资源。
- 分支事务中数据库的本地锁由本地事务管理,在分支事务Phase1结束时释放。同时,随着本地事务结束,连接也得以释放。
分支事务中数据的全局锁在事务协调器侧管理,在决议phase2全局提交时,全局锁马上可以释放。只有在决议全局回滚的情况下,全局锁才被持有至分支的Phase2结束。
Seata中有三个基础组件:
Transaction Coordinator(TC协调者):维护全局和分支事务的状态,驱动全局提交或回滚。
- Transaction Manager(TM事务管理):定义全局事务范围,开启、提交或回滚一个全局事务。
- Resource Mannager(RM资源管理):管理分支事务资源,与TC通讯并报告分支事务状态,管理本地事务的提交与回滚。
Seata的生命周期
- TX要求TC生产一个全局事务,并由TC生成一个全局事务XID返回。
- XID通过微服务调用链传播。
- RM向TC注册本地事务,将其注册到ID为XID的全局事务中。
- TM要求TC提交或回滚XID对应的全局事务。
- TC驱动XID对应的全局事务对应的所有的分支事务提交或回滚。
Seata提供AT,TCC,SAGA和XA事务模式
AT模式
前提:基于本地支持ACID事务的关系型数据库。java应用,通过JDBC访问数据库。
整体机制:两阶段提交协议的演变:
- 第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
第二阶段:
一阶段本地事务提交前,需要确保先拿到全局锁。
- 拿不到全局锁,不能提交本地事务。
- 拿全局锁的尝试被限制在一定范围内,超出范围将被放弃,并回滚本地事务,释放本地锁。
案例:
- tx1和tx2两个事务,分别对a表的m字段进行更新操作。tx1先开启本地事务,获取本地锁、进行更新操作m=1000-100=900。本地事务提交前,先获取全局锁,提交后释放本地锁。tx2开启本地事务,先拿到本地锁,进行更新m=900-100=800。本地事务提交前,尝试获取全局锁,tx1提交前,全局锁一直被tx1持有,tx2需要重试等待全局锁。
- tx1二阶段全局提交,释放全局锁,tx2拿到全局锁提交本地事务。
- 如果tx1二阶段全局回滚,则tx1重新获取本地锁,进行反哺的更新操作,实现分支的回滚。此时,如果tx2如果仍在等待全局锁,同时持有本地锁,则tx1的分支回滚会失败。分支回滚会一直重试,直到tx2的获取全局锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。
因为整个过程中全局锁在TX结束前一直被tx1持有,所以不会发生脏写的问题。
读隔离
在数据库本地事务隔离级别读已提交或以上的基础上,Seata(AT模式)的默认全局隔离级别是读未提交。
如果在特定场景下,必须要求全局的读已提交,目前Seata采用SELECT FOR UPDATE语句的代理。
整体机制
AT分支事务的业务逻辑:update user set name="zhangsan" where name="lisi"
一阶段:解析sql
- 查询前镜像: {id=1;name=lisi;age=18}
- 执行业务sql
- 查询后镜像:{id=1;name=zhangsan;age=18}
插入回滚日志,将查询前后镜像数据组成一条回滚日志,插入到
UNDO_LOG
表中{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "lisi"
}, {
"name": "age",
"type": 12,
"value": "18"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "zhangsan"
}, {
"name": "age",
"type": 12,
"value": "18"
}]
}],
"tableName": "user"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
提交前,想TC注册分支,申请user表中,id为1的记录的全局锁
- 本地事务提交,数据业务和上述
UNDO_LOG
一并提交。释放本地锁 - 将本地事务提交的结果上报给TC。
二阶段-回滚
- 收到TC的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过XID和Branch_ID查找相应的
UNDO_LOG
记录。 - 数据效验:拿
UNDO_LOG
中的后镜像数据与当前数据进行比较,当不同时需要根据策略进行处理。
根据UNDO_LOG
前镜像和业务sql相关信息生产并执行回滚语句。 - 提价本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给TC。
二阶段-提交
- 收到TC的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功给TC。
- 异步任务阶段的分支提交请求将异步和批量的删除相应的
UNDO_LOG
记录。TCC模式
所谓TCC模式,就是指把自定义的分支事务纳入到全局事务的管理中。
AT模式,基于支持本地ACID事务的关系型数据库:
一阶段prepare行为:在本地事务中,一并提交业务数据更新和相应的回滚日志。
二阶段commit行为:马上成功结束,自动异步批量清理回滚日照。
二阶段rollback行为:通过回滚日志,自动生产补偿操作,完成数据回滚。
TCC模式,不依赖于底层数据资源的事务支持。
一阶段prepare行为:调用自定义的prepare逻辑。
二阶段commit行为:调用自定义的commit逻辑。
二阶段rollback行为:调用自定义的rollback逻辑。Saga模式
是Seata提供的长事务解决方案,在Seata模式中,业务流程中每个参与者都提交本地事务,当出现一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
如图:T1-T3都是正向的业务流程,都对应着一个冲正逆向操作C1-C3。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会回退去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga正向服务和补偿服务也需要业务开发者实现,因此业务是入侵的。
Saga模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga模式是一种长事务解决方案。Saga模式使用场景
Saga模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。
事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供TCC要求的接口,可以使用Saga模式。Saga模式的优势与缺点
优势
- 一阶段提交本地数据库事务,无锁,高性能;
- 参与者可以采用事务驱动异步执行,高吞吐。
- 补偿服务是正向服务的”反向”,易于理解,易于实现。
缺点
Saga模式由于一阶段已经提交本地数据库事务,且没有进行预留动作,所以不能保证隔离性。
总结:AT,TCC,Saga,XA模式分析
四种分布式事务模式,分别在不同的时间被提出,每种模式都有它的适用场景。
AT模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。
- TCC模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
- Saga模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统。事务参与者无法提供TCC要求的接口,也可以使用Seata模式。
- XA模式是分布式强一致性的解决方案,但性能低使用较少。
Seata安装和配置
- 安装nacos,本案例使用nacos作为注册中心 https://github.com/alibaba/nacos/releases
- 以单机模式启动nacos:
startup -m standalone
- 安装和配置Seata http://seata.io/zh-cn/blog/download.html
- 解压后,进入conf目录,配置file.conf和registry.conf两个文件
- file.conf主要是数据库的配置:
- registry.conf是注册中心的配置
- 另外conf目录中还需要一个脚本文件:nacos-config.sh 用于对nacos进行初始化配置
在seata1.4.0中是没有的,需要自行创建 ```bash!/usr/bin/env bash
Copyright 1999-2019 Seata.io Group.
#Licensed under the Apache License, Version 2.0 (the “License”);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at、
#http://www.apache.org/licenses/LICENSE-2.0
#Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
while getopts “:h:p:g:t:u:w:” opt do case $opt in h) host=$OPTARG ;; p) port=$OPTARG ;; g) group=$OPTARG ;; t) tenant=$OPTARG ;; u) username=$OPTARG ;; w) password=$OPTARG ;; ?) echo “ USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] “ exit 1 ;; esac done
urlencode() { for ((i=0; i < ${#1}; i++)) do char=”${1:$i:1}” case $char in [a-zA-Z0-9.~_-]) printf $char ;; *) printf ‘%%%02X’ “‘$char” ;; esac done }
if [[ -z ${host} ]]; then host=localhost fi if [[ -z ${port} ]]; then port=8848 fi if [[ -z ${group} ]]; then group=”SEATA_GROUP” fi if [[ -z ${tenant} ]]; then tenant=”” fi if [[ -z ${username} ]]; then username=”” fi if [[ -z ${password} ]]; then password=”” fi
nacosAddr=$host:$port contentType=”content-type:application/json;charset=UTF-8”
echo “set nacosAddr=$nacosAddr” echo “set group=$group”
failCount=0 tempLog=$(mktemp -u) function addConfig() { curl -X POST -H “${contentType}” “http://$nacosAddr/nacos/v1/cs/configs?dataId=$(urlencode $1)&group=$group&content=$(urlencode $2)&tenant=$tenant&username=$username&password=$password” >”${tempLog}” 2>/dev/null if [[ -z $(cat “${tempLog}”) ]]; then echo “ Please check the cluster status. “ exit 1 fi if [[ $(cat “${tempLog}”) =~ “true” ]]; then echo “Set $1=$2 successfully “ else echo “Set $1=$2 failure “ (( failCount++ )) fi }
count=0 for line in $(cat $(dirname “$PWD”)/config.txt | sed s/[[:space:]]//g); do (( count++ )) key=${line%%=} value=${line#=} addConfig “${key}” “${value}” done
echo “=========================================================================” echo “ Complete initialization parameters, total-count:$count , failure-count:$failCount “ echo “=========================================================================”
if [[ ${failCount} -eq 0 ]]; then echo “ Init nacos config finished, please start seata-server. “ else echo “ init nacos config fail. “ fi
6. 在seata的根目录,与conf同级的目录下,还需要**config.txt **配置文件,默认也是没有的
- 只需要对mysql的配置进行修改
![image.png](https://cdn.nlark.com/yuque/0/2022/png/12860789/1648881205926-db87c4a7-34ca-446a-a444-35b33099460b.png#clientId=uf72d41ca-a992-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ub98541e0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=463&originWidth=965&originalType=url&ratio=1&rotation=0&showTitle=false&size=53113&status=done&style=none&taskId=u49ae9c42-a24e-4d57-a767-d2e92c28ac9&title=)
- 完整文件:
```bash
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=true
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
store.mode=file
store.lock.mode=file
store.session.mode=file
store.publicKey=xx
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=xx
store.redis.sentinel.sentinelHosts=xx
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=xx
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
- 在conf目录中,使用cmd进入命令行输入
sh nacos-config.sh 127.0.0.1
- 在nacos中可以看到出现了seata相关的配置
- 在seata数据库中,新建三个表
drop table if exists `global_table`;
create table `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`)
);
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`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`)
);
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
在项目相关的数据库中,新建表undo_log 用于记录撤销日志
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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
最后在bin目录中,启动命令行,执行seata-server.bat 启动Seata服务
- 项目应用Seata
- SpringCloud项目中有两个服务:订单服务和库存服务,基本业务是:
- 购买商品
- 插入订单
- 减少库存
订单详情表
DROP TABLE IF EXISTS `tb_order_detail`; CREATE TABLE `tb_order_detail` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单详情id ', `order_id` bigint(20) NOT NULL COMMENT '订单id', `sku_id` bigint(20) NOT NULL COMMENT 'sku商品id', `num` int(11) NOT NULL COMMENT '购买数量', `title` varchar(256) NOT NULL COMMENT '商品标题', `own_spec` varchar(1024) DEFAULT '' COMMENT '商品动态属性键值集', `price` bigint(20) NOT NULL COMMENT '价格,单位:分', `image` varchar(128) DEFAULT '' COMMENT '商品图片', PRIMARY KEY (`id`), KEY `key_order_id` (`order_id`) USING BTREE ) ENGINE=MyISAM AUTO_INCREMENT=131 DEFAULT CHARSET=utf8 COMMENT='订单详情表';
库存表
DROP TABLE IF EXISTS `tb_stock`; CREATE TABLE `tb_stock` ( `sku_id` bigint(20) NOT NULL COMMENT '库存对应的商品sku id', `seckill_stock` int(9) DEFAULT '0' COMMENT '可秒杀库存', `seckill_total` int(9) DEFAULT '0' COMMENT '秒杀总数量', `stock` int(9) NOT NULL COMMENT '库存数量', PRIMARY KEY (`sku_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='库存表,代表库存,秒杀库存等信息';
父项目定义了springboot、springcloud、springcloud-alibaba的版本
<parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.3.4.RELEASE</version> </parent> <packaging>pom</packaging> <groupId>com.example</groupId> <artifactId>demo-seata</artifactId> <name>demo-seata</name> <description>demo-seata</description> <modules> <module>order_service</module> <module>stock_service</module> </modules> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> </dependency> </dependencies> </dependencyManagement>
子项目的依赖定义了nacos和seata客户端
<parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.3.4.RELEASE</version> </parent> <groupId>com.example</groupId> <artifactId>order_service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>order_service</name> <description>order_service</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR8</spring-cloud.version> </properties> <dependencies> <!--没有web会导致加入注册中心失败--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.3.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.19</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter-test</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <!-- <version>2021.0.1.0</version>--> <version>2.2.7.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2.2.7.RELEASE</version> <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.3.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> </dependencies>
子项目配置文件 ```yaml server: port: 8001 spring: application: name: stock-service cloud: nacos: discovery:
server-addr: localhost:8848
alibaba: seata:
enabled: true enable-auto-data-source-proxy: true tx-service-group: SEATA_GROUP registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 username: nacos password: nacos config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP username: nacos password: nacos service: vgroup-mapping: SEATA_GROUP: default disable-global-transaction: false client: rm: report-success-enable: false
datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata?serverTimezone=UTC&useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC username: root password: 123456
mybatis
mybatis-plus: mapper-locations: classpath:mapper/**/Dao.xml #多模块mapper
实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.xxx.**.entity global-config: db-config: id-type: auto configuration: map-underscore-to-camel-case: true #驼峰结构
#打印sql执行难语句
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
17. 库存服务定义了减库存的方法
```java
@RestController
public class StockController {
@Autowired
private IStockService stockService;
@PutMapping("/stock")
public ResponseEntity<Stock> reduceSkuStock(@RequestParam("skuId")Long skuId,
@RequestParam("number")Integer number){
Stock stock = stockService.getById(skuId);
if(stock.getStock() < number){
throw new RuntimeException("库存不足,SkuId:" + skuId);
}
stock.setStock(stock.getStock() - number);
stockService.updateById(stock);
return ResponseEntity.ok(stock);
}
}
open启动类添加
@EnableFeignClients
```java @FeignClient(value=”stock-service”) public interface StockFeignClient {@PutMapping(“/stock”) public void reduceSkuStock(@RequestParam(“skuId”) Long skuId, @RequestParam(“number”) int number);
}
19. 订单服务在插入订单后,使用Feign调用了减库存的服务
```java
@Service
public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements IOrderDetailService {
//库存服务Feign
@Autowired
private StockFeignClient stockFeignClient;
// @Transactional
@GlobalTransactional(rollbackFor = {Exception.class})
@Override
public void makeOrder(OrderDetail orderDetail) {
this.save(orderDetail); //保存订单
int x = 11 / 0; //抛出异常
//减库存
stockFeignClient.reduceSkuStock(orderDetail.getSkuId(),orderDetail.getNum());
}
}
- 插订单和减库存属于两个服务,传统的@Transactional已经不能保证它们的原子性了。这里使用了Seata提供的@GlobalTransactional全局事务注解,出现任何异常后都能实现业务回滚。
测试用例:
@RunWith(SpringRunner.class) @SpringBootTest public class OrderServiceApplicationTests { @Autowired private IOrderDetailService orderDetailService; @Test public void testOrder() { OrderDetail orderDetail = new OrderDetail(); orderDetail.setNum(100); orderDetail.setOrderId(9999L); orderDetail.setPrice(9999L); orderDetail.setSkuId(27359021728L); orderDetail.setTitle(UUID.randomUUID().toString()); orderDetailService.makeOrder(orderDetail); } }
运行后看到启动了全局事务,发生异常后,两个服务也都能成功回滚。
注意:
缺少web依赖,服务不能注册到nacos
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>