目录

springcloudAlibaba_seata 快速启动

官网快速启动

用例说明

用例

用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:

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

简单来说就是:下订单–>扣库存–>减账户(余额)

架构图

http://img.cana.space/picStore/20201117003919.png

准备工作

库表

  1. 建立数据库

    要求:具有InnoDB引擎的MySQL。

    注意: 实际上,在示例用例中,这3个服务应该有3个数据库。 但是,为了简单起见,我们只创建一个数据库并配置3个数据源。

  2. 创建 UNDO_LOG 表

    使用脚本:seata-1.3.0/script/client/at/db/mysql.sql,脚本如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    -- 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`
    (
        `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT COMMENT 'increment id',
        `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
        `xid`           VARCHAR(100) 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     NOT NULL COMMENT 'create datetime',
        `log_modified`  DATETIME     NOT NULL COMMENT 'modify datetime',
        PRIMARY KEY (`id`),
        UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
      AUTO_INCREMENT = 1
      DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
    
  3. 为示例业务创建表

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    DROP TABLE IF EXISTS `storage_tbl`;
    CREATE TABLE `storage_tbl` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `commodity_code` varchar(255) DEFAULT NULL,
      `count` int(11) DEFAULT 0,
      PRIMARY KEY (`id`),
      UNIQUE KEY (`commodity_code`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
       
       
    DROP TABLE IF EXISTS `order_tbl`;
    CREATE TABLE `order_tbl` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` varchar(255) DEFAULT NULL,
      `commodity_code` varchar(255) DEFAULT NULL,
      `count` int(11) DEFAULT 0,
      `money` int(11) DEFAULT 0,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
       
       
    DROP TABLE IF EXISTS `account_tbl`;
    CREATE TABLE `account_tbl` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` varchar(255) DEFAULT NULL,
      `money` int(11) DEFAULT 0,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
       
    -- 初始化库存模拟数据
    delete from seata_demo.storage_tbl;
    delete from seata_demo.order_tbl;
    delete from seata_demo.account_tbl;
    INSERT INTO seata_demo.storage_tbl (commodity_code, count) VALUES ('product-1', 10);
    INSERT INTO seata_demo.account_tbl (user_id, money) VALUES ('david001', 100);
    

工程

先演示没有分布式事务控制时的异常情况

  • cloudalibaba-seata-business2001:业务逻辑
  • cloudalibaba-seata-service2002:模拟3个业务分别操作3个数据源,分别是对给定的商品扣除仓储数量,根据采购需求创建订单,从用户帐户中扣除余额

工程调用关系:

http://img.cana.space/picStore/20201117003919.png

cloudalibaba-seata-service2002

工程目录结构

20201117181124

pom

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>com.eh</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-seata-storage2002</artifactId>

    <dependencies>
        <!--服务注册与发现nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--整合web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入通用包,主要使用CommonResult类-->
        <dependency>
            <groupId>org.eh</groupId>
            <artifactId>cloud-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--数据源配置 begin-->
        <!--druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--数据源配置 end-->
    </dependencies>

</project>

yml

使用3个数据源模拟3个数据库的情况

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
server:
  port: 2002

spring:
  application:
    name: cloudalibaba-seata-service2002
  cloud:
    nacos:
      discovery:
        #配置nacos地址
        server-addr: localhost:8848
  datasource:
    storage:
      type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
      driver-class-name: com.mysql.cj.jdbc.Driver             # mysql驱动包
      # springboot2.0以上配置多数据源,需要将url改成jdbc-url
      # spring.datasource.url 数据库的 JDBC URL
      # spring.datasource.jdbc-url 用来重写自定义连接池
      # 官方给出的解释是:因为连接池的实际类型没有被公开,所以在您的自定义数据源的元数据中没有生成密钥,
      # 而且在IDE中没有完成(因为DataSource接口没有暴露属性)。另外,如果您碰巧在类路径上有Hikari,那么这个基本设置就不起作用了,
      # 因为Hikari没有url属性(但是确实有一个jdbcUrl属性)。在这种情况下,您必须重写您的配置
      jdbc-url: jdbc:mysql://localhost:3306/seata_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: root
      password: 333
    order:
      type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
      driver-class-name: com.mysql.cj.jdbc.Driver             # mysql驱动包
      jdbc-url: jdbc:mysql://localhost:3306/seata_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: root
      password: 333
    account:
      type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
      driver-class-name: com.mysql.cj.jdbc.Driver             # mysql驱动包
      jdbc-url: jdbc:mysql://localhost:3306/seata_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: root
      password: 333

业务类

数据源配置

  • AccountDataSourceConfig

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    
    package com.eh.cloud.seata.service.config;
      
    import lombok.SneakyThrows;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.SqlSessionTemplate;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
      
    import javax.sql.DataSource;
      
      
    @MapperScan(basePackages = "com.eh.cloud.seata.service.dao.account", sqlSessionTemplateRef = "accountSqlSessionTemplate")
    @Configuration
    public class AccountDataSourceConfig {
      
        // 数据源
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.account")
        public DataSource accountDataSource() {
            return DataSourceBuilder.create().build();
        }
      
        // sqlSessionFactory
        @Bean
        @SneakyThrows
        public SqlSessionFactory accountSqlSessionFactory(@Qualifier("accountDataSource") DataSource dataSource) {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSource);
            // 如果使用了maaper文件还要加上下面这行
            // sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/example/datasources/mapper/xxx/*.xml"));
            return sqlSessionFactoryBean.getObject();
        }
      
        // sqlSessionTemplate
        @Bean
        public SqlSessionTemplate accountSqlSessionTemplate(@Qualifier("accountSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
      
        // 事务管理器,seata演示中不需要,因为是单条sql操作
        @Bean
        public DataSourceTransactionManager accountTransactionManager(@Qualifier("accountDataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
      
    }
    
  • OrderDataSourceConfig

    参照:AccountDataSourceConfig

  • StorageDataSourceConfig

    参照:AccountDataSourceConfig

dao层

  • AccountMapper

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    package com.eh.cloud.seata.service.dao.account;
      
    import com.eh.cloud.seata.service.entity.Account;
    import org.apache.ibatis.annotations.Update;
      
    public interface AccountMapper {
      
        @Update("update account_tbl set money = money - #{money} where user_id = #{userId}")
        int decreateMoney(Account account);
    }
    
  • OrderMapper

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    package com.eh.cloud.seata.service.dao.order;
      
    import com.eh.cloud.seata.service.entity.Order;
    import org.apache.ibatis.annotations.Insert;
      
    public interface OrderMapper {
      
        @Insert("insert into order_tbl(user_id, commodity_code, count, money) values(#{userId}, #{commodityCode}, #{count}, #{money})")
        int createOrder(Order order);
    }
    
  • StorageMapper

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    package com.eh.cloud.seata.service.dao.storage;
      
    import com.eh.cloud.seata.service.entity.Storage;
    import org.apache.ibatis.annotations.Update;
      
    public interface StorageMapper {
      
        @Update("update storage_tbl set count = count - #{count} where commodity_code = #{commodityCode}")
        int decreateStorage(Storage storage);
      
    }
    

主启动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.eh.cloud.seata.service;

import com.eh.cloud.seata.service.dao.account.AccountMapper;
import com.eh.cloud.seata.service.dao.order.OrderMapper;
import com.eh.cloud.seata.service.dao.storage.StorageMapper;
import com.eh.cloud.seata.service.entity.Account;
import com.eh.cloud.seata.service.entity.Order;
import com.eh.cloud.seata.service.entity.Storage;
import com.eh.cloud2020.common.entity.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
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.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataStorageMain2002 {
    public static void main(String[] args) {
        SpringApplication.run(SeataStorageMain2002.class, args);
    }

    @RestController
    class StorageController {

        @Autowired
        private StorageMapper storageMapper;

        @PostMapping("/storage/decrease")
        public CommonResult<String> decreateStorage(@RequestBody Map<String, String> request) {
            try {
                Storage storage = new Storage();
                storage.setCommodityCode(request.get("commodityCode"));
                storage.setCount(Integer.valueOf(request.get("count")));
                int i = storageMapper.decreateStorage(storage);
                return CommonResult.success("成功更新:" + i);
            } catch (Exception e) {
                return CommonResult.error(80001, "库存服务出错");
            }

        }
    }

    @RestController
    class OrderController {

        @Autowired
        private OrderMapper orderMapper;
        @Autowired
        private AccountMapper accountMapper;

        @PostMapping("/order/create")
        public CommonResult<String> createOrder(@RequestBody Map<String, String> request) {
            try {
                // 创建订单
                Order order = new Order();
                order.setCommodityCode(request.get("userId"));
                order.setCommodityCode(request.get("commodityCode"));
                order.setCount(Integer.valueOf(request.get("count")));
                order.setMoney(Integer.valueOf(request.get("money")));
                int i = orderMapper.createOrder(order);
                // 扣减余额
                Account account = new Account();
                account.setUserId(request.get("userId"));
                account.setMoney(Integer.valueOf(request.get("money")));
                // 演示异常情况时打开
                int m = 1 / 0;
                int j = accountMapper.decreateMoney(account);
                return CommonResult.success("成功创建订单并扣减余额:" + (i + j));
            } catch (Exception e) {
                return CommonResult.error(80002, "订单服务出错");
            }
        }
    }
}

cloudalibaba-seata-business2001

工程目录结构

20201117181512

pom

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>com.eh</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloudalibaba-seata-business2001</artifactId>

    <dependencies>
        <!--整合openFeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--服务注册与发现nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--引入通用包,主要使用CommonResult类-->
        <dependency>
            <groupId>org.eh</groupId>
            <artifactId>cloud-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--引入jdbc starter和数据库驱动,做数据初始化使用,也可以不加以下依赖,自己观察数据库-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

</project>

yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server:
  port: 2001

spring:
  application:
    name: cloudalibaba-seata-business2001
  cloud:
    nacos:
      discovery:
        #配置nacos地址
        server-addr: localhost:8848
  datasource:
    username: root
    password: 333
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_demo?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&allowMultiQueries=true
    initialization-mode: always
    schema: classpath:init.sql

#设置feign客户端超时时间(ribbon默认超时时间1s,测试过程中有超过1s的情况,不利于测试,改成3s)
ribbon:
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ReadTimeout: 3000
  #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ConnectTimeout: 3000

init.sql

1
2
3
4
5
6
-- 初始化库存模拟数据
delete from seata_demo.storage_tbl;
delete from seata_demo.order_tbl;
delete from seata_demo.account_tbl;
INSERT INTO seata_demo.storage_tbl (commodity_code, count) VALUES ('product-1', 10);
INSERT INTO seata_demo.account_tbl (user_id, money) VALUES ('david001', 100);

logback.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<configuration scanPeriod="60 seconds" debug="false">

    <timestamp key="dt" datePattern="yyyyMMdd HH:mm:ss"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--高亮-->
            <pattern>${dt} [%thread] %highlight(%-5level) %cyan(%logger{50}) - %msg%n</pattern>
        </encoder>
        <withJansi>true</withJansi>
    </appender>

    <!-- <logger name="com.eh.springcloud.payment8001.com.eh.cloud.seata.storage.dao" level="debug"/>
     <logger name="org.mybatis" level="debug"/>-->

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

业务类

SeataService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.eh.cloud.seata.business.third;

import com.eh.cloud2020.common.entity.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.Map;

@FeignClient("cloudalibaba-seata-service2002")
public interface SeataService {

    @PostMapping("/account/decrease")
    CommonResult<String> decreateAccount(Map<String, String> request);

    @PostMapping("/order/create")
    CommonResult<String> createOrder(Map<String, String> request);

    @PostMapping("/storage/decrease")
    CommonResult<String> decreateStorage(Map<String, String> request);
}

BusinessService

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.eh.cloud.seata.business.service;

import com.eh.cloud.seata.business.third.SeataService;
import com.eh.cloud2020.common.entity.CommonResult;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
public class BusinessService {

    @Autowired
    private SeataService seataService;

    public void test() {
        log.info("=========>开始测试");
        log.info("用户====>用户id:{},余额:{}", "david001", "100");
        log.info("库存====>商品编号:{},商品剩余数量:{}", "product-1", "10");
        // 减库存
        Map<String, String> storageReq = new HashMap() {{
            put("commodityCode", "product-1");
            put("count", "1");
        }};
        log.info("==========>开始扣减库存");
        CommonResult<String> storageRet = seataService.decreateStorage(storageReq);
        // 建订单
        Map<String, String> orderReq = new HashMap() {{
            put("userId", "david001");
            put("commodityCode", "product-1");
            put("count", "1");
            put("money", "25");
        }};
        log.info("==========>开始创建订单");
        CommonResult<String> orderRet = seataService.createOrder(orderReq);

        if (orderRet.isOk() && storageRet.isOk()) {
            log.info("========>用户购买商品成功");
        } else {
            throw new RuntimeException("用户购买商品失败");
        }
    }
}

主启动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.eh.cloud.seata.business;

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

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class SeataBusinessMain2001 {
    public static void main(String[] args) {
        SpringApplication.run(SeataBusinessMain2001.class, args);
    }
}

测试类

SeataBusinessTest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.eh.cloud.seata.business;

import com.eh.cloud.seata.business.service.BusinessService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Map;

@Slf4j
@SpringBootTest
public class SeataBusinessTest {

    @Autowired
    private BusinessService businessService;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void test() {
        businessService.test();
    }

    @Test
    public void queryResult() {
        // 查询数据库变化情况
        log.info("=============> 结果查询");
        Map<String, Object> storateResult = jdbcTemplate.queryForMap("select commodity_code, count from storage_tbl limit 1");
        log.info("库存情况==========>产品:{},剩余数量:{}", storateResult.get("commodity_code"), storateResult.get("count"));
        Map<String, Object> orderResult = jdbcTemplate.queryForMap("select id, commodity_code, count, money from order_tbl limit 1");
        log.info("订单情况==========>创建订单, id:{},产品编号:{},购买数量:{}", orderResult.get("id"), orderResult.get("commodity_code"), orderResult.get("count"));
        Map<String, Object> accountResult = jdbcTemplate.queryForMap("select user_id, money from account_tbl limit 1");
        log.info("用户余额情况==========>用户:{},余额:{}", accountResult.get("user_id"), accountResult.get("money"));
    }
}

执行结果:

http://img.cana.space/picStore/20201117181956.png

从日志中可以看到库存减少了1,订单也成功创建,但是用户余额没有扣减。接下来我们加入seata框架到应用中来处理分布式事务。

引入Seata处理分布式事务

pom依赖说明:部署指南

yml配置内容:进入 资源目录 seata/script/client/spring/ ,展示的就是 seata 整合 Spring 的全部配置内容,提供了 .properties.yml 两种格式的配置。详细的配置项还挺多,此处就不粘贴了,你可以点击 资源目录 查看。

分布式事务处理过程

http://img.cana.space/picStore/20201117214456.png

服务调用版本

改造cloudalibaba-seata-service2002

pom,引入seta

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!--seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <!--确保版本与服务器端保持一致-->
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--我的服务器端版本是1.2.0-->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.2.0</version>
</dependency>

yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  enable-auto-data-source-proxy: true
  # seata服务
  service:
    vgroup-mapping:
      my_test_tx_group: default  # 此处key需要与tx-service-group的value一致,否则会报 no available service 'null' found, please make sure registry config correct 异常
    grouplist:
      default: localhost:8091
    enable-degrade: false
    disable-global-transaction: false
  # seata配置,使用nacos作为配置中心
  config:
    type: nacos
    nacos:
      namespace:
      serverAddr: localhost:8848
      group: SEATA_GROUP
      userName: ""
      password: ""
  # seata服务所在注册中心
  registry:
    type: nacos
    nacos:
      application: seata-server  # 此处名称需和 seata server 服务端 application一致,否则会报 no available service 'null' found, please make sure registry config correct 异常
      server-addr: localhost:8848
      namespace:
      userName: ""
      password: ""

cloudalibaba-seata-business2001,pom和yml 引入seta依赖和配置,同上面cloudalibaba-seata-business2002

在测试方法上增加注解@GlobalTransactional 即可使用seata的分布式事务功能

1
2
 @GlobalTransactional
    public void test() {

执行测试方法,然后在数据库执行如下查询:

1
2
3
select * from seata_demo.storage_tbl;
select * from seata_demo.order_tbl;
select * from seata_demo.account_tbl;

可以发现数据保持一致。

多数据源版本

pom和yaml配置不变,增加以下测试类

业务类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.eh.cloud.seata.service;

import com.eh.cloud.seata.dao.account.AccountMapper;
import com.eh.cloud.seata.dao.order.OrderMapper;
import com.eh.cloud.seata.dao.storage.StorageMapper;
import com.eh.cloud.seata.entity.Account;
import com.eh.cloud.seata.entity.Order;
import com.eh.cloud.seata.entity.Storage;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class OrderService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private StorageMapper storageMapper;
    @Autowired
    private OrderMapper orderMapper;

    @GlobalTransactional
    public void placeOrder() {
        log.info("=========>开始测试");
        log.info("用户====>用户id:{},余额:{}", "david001", "100");
        log.info("库存====>商品编号:{},商品剩余数量:{}", "product-1", "10");
        // 减库存
        Storage storage = new Storage();
        storage.setCommodityCode("product-1");
        storage.setCount(1);
        log.info("==========>开始扣减库存");
        storageMapper.decreateStorage(storage);
        // 建订单
        Order order = new Order();
        order.setUserId("david001");
        order.setCommodityCode("product-1");
        order.setCount(1);
        order.setMoney(25);
        log.info("==========>开始创建订单");
        orderMapper.createOrder(order);
        // 扣减余额
        Account account = new Account();
        account.setUserId("david001");
        account.setMoney(25);
        // 演示异常情况时打开
        int m = 1 / 0;
        log.info("==========>开始扣减余额");
        accountMapper.decreateMoney(account);
    }
}

controller

1
2
3
4
5
6
7
@Autowired
private OrderService orderService;

@GetMapping("/test")
public void test() {
    orderService.placeOrder();
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
GET http://localhost:2002/test

HTTP/1.1 500 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 17 Nov 2020 13:17:07 GMT
Connection: close

{
  "timestamp": "2020-11-17T13:17:07.947+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "/ by zero",
  "path": "/test"
}

Response code: 500; Time: 446ms; Content length: 126 bytes

多次调用,查看数据库可以看到库存数量和用户余额不变,debug可以看到undo_log表和seta服务器配置的数据库中表的变化情况。