Seata分布式事务

传统2PC的问题在seata中得到解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务。
是工作在应用层的中间件。主要的优点是性能较好,且长时间不占用连接资源,它以高效并对业务0侵入的方式解决微服务场景下面临的分布式事务问题。目前提供AT模式(即2pc)及TCC模式的分布式事务解决方案。

Seata的设计思想

把事务的信息以日志的方式记录下来。这种处理方式,实际上是对传统两阶段提交的一种改进和优化:

  1. 传统两阶段提交协议是阻塞协议,性能差
  2. 传统两阶段提交协议高可用性不好
  3. 传统两阶段提交协议的全局事务隔离机制不支持
  4. 根据八二原则,80%的涉及到全局事务的业务是能正常完成并提交的。

因此,seata采取的做法是,一个事务分支的数据库操作执行完成后,马上进行本地事务的提交,从而释放相关的数据库资源。

  • 分支事务中数据库的本地锁由本地事务管理,在分支事务Phase1结束时释放。同时,随着本地事务结束,连接也得以释放。
  • 分支事务中数据的全局锁在事务协调器侧管理,在决议phase2全局提交时,全局锁马上可以释放。只有在决议全局回滚的情况下,全局锁才被持有至分支的Phase2结束。

    Seata中有三个基础组件:

  • Transaction Coordinator(TC协调者):维护全局和分支事务的状态,驱动全局提交或回滚。

  • Transaction Manager(TM事务管理):定义全局事务范围,开启、提交或回滚一个全局事务。
  • Resource Mannager(RM资源管理):管理分支事务资源,与TC通讯并报告分支事务状态,管理本地事务的提交与回滚。

image.png

Seata的生命周期

  • TX要求TC生产一个全局事务,并由TC生成一个全局事务XID返回。
  • XID通过微服务调用链传播。
  • RM向TC注册本地事务,将其注册到ID为XID的全局事务中。
  • TM要求TC提交或回滚XID对应的全局事务。
  • TC驱动XID对应的全局事务对应的所有的分支事务提交或回滚。

image.png

Seata提供AT,TCC,SAGA和XA事务模式

AT模式

前提:基于本地支持ACID事务的关系型数据库。java应用,通过JDBC访问数据库。
整体机制:两阶段提交协议的演变:

  • 第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 第二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

      写隔离

  • 一阶段本地事务提交前,需要确保先拿到全局锁

  • 拿不到全局锁,不能提交本地事务。
  • 全局锁的尝试被限制在一定范围内,超出范围将被放弃,并回滚本地事务,释放本地锁。

案例:

  1. tx1和tx2两个事务,分别对a表的m字段进行更新操作。tx1先开启本地事务,获取本地锁、进行更新操作m=1000-100=900。本地事务提交前,先获取全局锁,提交后释放本地锁。tx2开启本地事务,先拿到本地锁,进行更新m=900-100=800。本地事务提交前,尝试获取全局锁,tx1提交前,全局锁一直被tx1持有,tx2需要重试等待全局锁。
  2. tx1二阶段全局提交,释放全局锁,tx2拿到全局锁提交本地事务。
  3. 如果tx1二阶段全局回滚,则tx1重新获取本地锁,进行反哺的更新操作,实现分支的回滚。此时,如果tx2如果仍在等待全局锁,同时持有本地锁,则tx1的分支回滚会失败。分支回滚会一直重试,直到tx2的获取全局锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1的分支回滚最终成功。
  4. 因为整个过程中全局锁在TX结束前一直被tx1持有,所以不会发生脏写的问题。

    读隔离

    在数据库本地事务隔离级别读已提交或以上的基础上,Seata(AT模式)的默认全局隔离级别是读未提交
    如果在特定场景下,必须要求全局的读已提交,目前Seata采用SELECT FOR UPDATE语句的代理。
    整体机制
    AT分支事务的业务逻辑:update user set name="zhangsan" where name="lisi"
    一阶段:

  5. 解析sql

  6. 查询前镜像: {id=1;name=lisi;age=18}
  7. 执行业务sql
  8. 查询后镜像:{id=1;name=zhangsan;age=18}
  9. 插入回滚日志,将查询前后镜像数据组成一条回滚日志,插入到UNDO_LOG表中

    1. {
    2. "branchId": 641789253,
    3. "undoItems": [{
    4. "afterImage": {
    5. "rows": [{
    6. "fields": [{
    7. "name": "id",
    8. "type": 4,
    9. "value": 1
    10. }, {
    11. "name": "name",
    12. "type": 12,
    13. "value": "lisi"
    14. }, {
    15. "name": "age",
    16. "type": 12,
    17. "value": "18"
    18. }]
    19. }],
    20. "tableName": "product"
    21. },
    22. "beforeImage": {
    23. "rows": [{
    24. "fields": [{
    25. "name": "id",
    26. "type": 4,
    27. "value": 1
    28. }, {
    29. "name": "name",
    30. "type": 12,
    31. "value": "zhangsan"
    32. }, {
    33. "name": "age",
    34. "type": 12,
    35. "value": "18"
    36. }]
    37. }],
    38. "tableName": "user"
    39. },
    40. "sqlType": "UPDATE"
    41. }],
    42. "xid": "xid:xxx"
    43. }
  10. 提交前,想TC注册分支,申请user表中,id为1的记录的全局锁

  11. 本地事务提交,数据业务和上述UNDO_LOG一并提交。释放本地锁
  12. 将本地事务提交的结果上报给TC。

二阶段-回滚

  1. 收到TC的分支回滚请求,开启一个本地事务,执行如下操作。
  2. 通过XID和Branch_ID查找相应的UNDO_LOG记录。
  3. 数据效验:拿UNDO_LOG中的后镜像数据与当前数据进行比较,当不同时需要根据策略进行处理。
    根据UNDO_LOG前镜像和业务sql相关信息生产并执行回滚语句。
  4. 提价本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给TC。

二阶段-提交

  1. 收到TC的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功给TC。
  2. 异步任务阶段的分支提交请求将异步和批量的删除相应的UNDO_LOG记录。

    TCC模式

    所谓TCC模式,就是指把自定义的分支事务纳入到全局事务的管理中。
    AT模式,基于支持本地ACID事务的关系型数据库:
    一阶段prepare行为:在本地事务中,一并提交业务数据更新和相应的回滚日志。
    二阶段commit行为:马上成功结束,自动异步批量清理回滚日照。
    二阶段rollback行为:通过回滚日志,自动生产补偿操作,完成数据回滚。
    TCC模式,不依赖于底层数据资源的事务支持。
    一阶段prepare行为:调用自定义的prepare逻辑。
    二阶段commit行为:调用自定义的commit逻辑。
    二阶段rollback行为:调用自定义的rollback逻辑。

    Saga模式

    是Seata提供的长事务解决方案,在Seata模式中,业务流程中每个参与者都提交本地事务,当出现一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
    image.png
    如图: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安装和配置

  1. 安装nacos,本案例使用nacos作为注册中心 https://github.com/alibaba/nacos/releases
  2. 以单机模式启动nacos: startup -m standalone
  3. 安装和配置Seata http://seata.io/zh-cn/blog/download.html
  4. 解压后,进入conf目录,配置file.conf和registry.conf两个文件
    • file.conf主要是数据库的配置:

image.png

  • registry.conf是注册中心的配置

image.png

  1. 另外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

  1. 6. seata的根目录,与conf同级的目录下,还需要**config.txt **配置文件,默认也是没有的
  2. - 只需要对mysql的配置进行修改
  3. ![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=)
  4. - 完整文件:
  5. ```bash
  6. transport.type=TCP
  7. transport.server=NIO
  8. transport.heartbeat=true
  9. transport.enableClientBatchSendRequest=true
  10. transport.threadFactory.bossThreadPrefix=NettyBoss
  11. transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
  12. transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
  13. transport.threadFactory.shareBossWorker=false
  14. transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
  15. transport.threadFactory.clientSelectorThreadSize=1
  16. transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
  17. transport.threadFactory.bossThreadSize=1
  18. transport.threadFactory.workerThreadSize=default
  19. transport.shutdown.wait=3
  20. service.vgroupMapping.my_test_tx_group=default
  21. service.default.grouplist=127.0.0.1:8091
  22. service.enableDegrade=false
  23. service.disableGlobalTransaction=false
  24. client.rm.asyncCommitBufferLimit=10000
  25. client.rm.lock.retryInterval=10
  26. client.rm.lock.retryTimes=30
  27. client.rm.lock.retryPolicyBranchRollbackOnConflict=true
  28. client.rm.reportRetryCount=5
  29. client.rm.tableMetaCheckEnable=false
  30. client.rm.tableMetaCheckerInterval=60000
  31. client.rm.sqlParserType=druid
  32. client.rm.reportSuccessEnable=false
  33. client.rm.sagaBranchRegisterEnable=false
  34. client.rm.tccActionInterceptorOrder=-2147482648
  35. client.tm.commitRetryCount=5
  36. client.tm.rollbackRetryCount=5
  37. client.tm.defaultGlobalTransactionTimeout=60000
  38. client.tm.degradeCheck=false
  39. client.tm.degradeCheckAllowTimes=10
  40. client.tm.degradeCheckPeriod=2000
  41. client.tm.interceptorOrder=-2147482648
  42. store.mode=file
  43. store.lock.mode=file
  44. store.session.mode=file
  45. store.publicKey=xx
  46. store.file.dir=file_store/data
  47. store.file.maxBranchSessionSize=16384
  48. store.file.maxGlobalSessionSize=512
  49. store.file.fileWriteBufferCacheSize=16384
  50. store.file.flushDiskMode=async
  51. store.file.sessionReloadReadSize=100
  52. store.db.datasource=druid
  53. store.db.dbType=mysql
  54. store.db.driverClassName=com.mysql.jdbc.Driver
  55. store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
  56. store.db.user=root
  57. store.db.password=123456
  58. store.db.minConn=5
  59. store.db.maxConn=30
  60. store.db.globalTable=global_table
  61. store.db.branchTable=branch_table
  62. store.db.queryLimit=100
  63. store.db.lockTable=lock_table
  64. store.db.maxWait=5000
  65. store.redis.mode=single
  66. store.redis.single.host=127.0.0.1
  67. store.redis.single.port=6379
  68. store.redis.sentinel.masterName=xx
  69. store.redis.sentinel.sentinelHosts=xx
  70. store.redis.maxConn=10
  71. store.redis.minConn=1
  72. store.redis.maxTotal=100
  73. store.redis.database=0
  74. store.redis.password=xx
  75. store.redis.queryLimit=100
  76. server.recovery.committingRetryPeriod=1000
  77. server.recovery.asynCommittingRetryPeriod=1000
  78. server.recovery.rollbackingRetryPeriod=1000
  79. server.recovery.timeoutRetryPeriod=1000
  80. server.maxCommitRetryTimeout=-1
  81. server.maxRollbackRetryTimeout=-1
  82. server.rollbackRetryTimeoutUnlockEnable=false
  83. server.distributedLockExpireTime=10000
  84. client.undo.dataValidation=true
  85. client.undo.logSerialization=jackson
  86. client.undo.onlyCareUpdateColumns=true
  87. server.undo.logSaveDays=7
  88. server.undo.logDeletePeriod=86400000
  89. client.undo.logTable=undo_log
  90. client.undo.compress.enable=true
  91. client.undo.compress.type=zip
  92. client.undo.compress.threshold=64k
  93. log.exceptionRate=100
  94. transport.serialization=seata
  95. transport.compressor=none
  96. metrics.enabled=false
  97. metrics.registryType=compact
  98. metrics.exporterList=prometheus
  99. metrics.exporterPrometheusPort=9898
  1. 在conf目录中,使用cmd进入命令行输入 sh nacos-config.sh 127.0.0.1

image.png

  1. 在nacos中可以看到出现了seata相关的配置

image.png

  1. 在seata数据库中,新建三个表

image.png

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`)
);
  1. 在项目相关的数据库中,新建表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;
    
  2. 最后在bin目录中,启动命令行,执行seata-server.bat 启动Seata服务

image.png

  1. 项目应用Seata

image.png

  1. 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='库存表,代表库存,秒杀库存等信息';
    
  1. 父项目定义了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>
    
  2. 子项目的依赖定义了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>
    
  3. 子项目配置文件 ```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);
    }
}
  1. 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()); 
    }
}
  1. 插订单和减库存属于两个服务,传统的@Transactional已经不能保证它们的原子性了。这里使用了Seata提供的@GlobalTransactional全局事务注解,出现任何异常后都能实现业务回滚。
  2. 测试用例:

    @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);
    }
    }
    
  3. 运行后看到启动了全局事务,发生异常后,两个服务也都能成功回滚。

注意:
缺少web依赖,服务不能注册到nacos

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>