分布式事务(八):Seata之AT模式应用

  Seata中AT模式的官方文档地址:https://seata.io/zh-cn/docs/dev/mode/at-mode.html

  AT模式是一种无侵入的分布式事务解决方案,基于两阶段提交协议实现的,用户的业务SQL处理作为一阶段,Seata会根据一阶段的执行结果自动判断二阶段的提交或回滚。

1、整体机制

Seata的AT模式是基于两阶段提交协议演变而来的:

  一阶段:

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

  二阶段:

    提交:提交异步化

    回滚:通过一阶段的回滚日志数据 undo_log 进行反向补偿。

2、应用

  拿官方文档的案例进行操作,首先创建商品表 stock:

CREATE TABLE `stock` (
  `product_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `name` varchar(100) DEFAULT NULL COMMENT '商品名称',
  `stock` int DEFAULT '0' COMMENT '库存数量',
  PRIMARY KEY (`product_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='产品表';


INSERT INTO `stock`(`product_id`, `name`, `stock`) VALUES (1, 'JAVA编程思想', 2000);

  订单表order:

CREATE TABLE `order` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `product_id` bigint DEFAULT NULL COMMENT '商品id',
  `product_num` int DEFAULT NULL COMMENT '商品数量',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

  回滚表undo_log:

-- 注意此处0.7.0+ 增加字段 context
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2.1、创建订单、库存服务

  订单、库存服务目录结构如下:

0        0

  订单、库存服务的POM依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.3.12.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>2.3.12.RELEASE</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <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>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <version>2.2.9.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.23</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
</dependencies>

2.1.1、订单服务

1.1、配置文件信息

  appication.yml,文件中关于seata的配置需要与Seata-Server中的application.yml保持一致,Seata-Server的搭建详见分布式事务(七):Seata-Server的搭建

server:
  port: 6021
spring:
  application:
    name: seata-order
  # druid数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.33.55:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.33.55:8848
    alibaba:
      seata:
        tx-service-group: SEATA-GROUP   # 事务组, 自定义需要与Seata-Server中的配置保持一致

seata:
  tx-service-group: trxgroup
  service:
    vgroup-mapping:
      trxgroup: default
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: 192.168.33.55:8848
      namespace:
      group: SEATA_GROUP
      username:
      password:
      data-id: seataServer.properties
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.33.55:8848
      group: SEATA_GROUP
      namespace:
      cluster: default
      username: nacos
      password: nacos
  store:
    # support: file 、 db 、 redis
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.33.55:3306/seata?rewriteBatchedStatements=true
      user: root
      password: root
      min-conn: 5
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 100
      max-wait: 5000

#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
  # 建立连接后从服务器读取到可用资源所用的时间 6s
  ReadTimeout: 6000
  # 建立连接所用的时间   6s
  ConnectTimeout: 6000

1.2、订单服务业务代码

  Seata事务模式配置,DataSourceConfig.java详情如下:

 1 package com.snails.conf;
 2 
 3 import com.alibaba.druid.pool.DruidDataSource;
 4 import io.seata.rm.datasource.DataSourceProxy;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
 7 import org.springframework.context.annotation.Bean;
 8 import org.springframework.context.annotation.Configuration;
 9 import javax.sql.DataSource;
10 
11 @Configuration
12 public class DataSourceConfig {
13 
14     @Autowired
15     DataSourceProperties dataSourceProperties;
16 
17     /**
18      *  SEATA事务模式配置
19      * @param dataSourceProperties
20      * @return
21      */
22     @Bean
23     public DataSource dataSource(DataSourceProperties dataSourceProperties){
24         DruidDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
25         // AT 模式
26         return new DataSourceProxy(dataSource);
27         // XA 模式
28 //        return new DataSourceProxyXA(dataSource);
29     }
30 }

  orderMapper详情如下:

 1 package com.snails.mapper;
 2 import org.apache.ibatis.annotations.Insert;
 3 import org.apache.ibatis.annotations.Mapper;
 4 import org.apache.ibatis.annotations.Param;
 5 
 6 @Mapper
 7 public interface OrderMapper {
 8 
 9     @Insert("INSERT INTO `order`(`product_id`, `product_num`) VALUES (#{productId}, #{productNum})")
10     Integer addOrder(@Param("productId") Integer productId,
11                     @Param("productNum") Integer productNum);
12 
13 }

  OrderService详情如下:

1 package com.snails.service;
2 
3 public interface OrderService {
4     Integer handleOrder(Integer productId);
5 }

  OpenFeignService详情如下:

 1 package com.snails.openfeign;
 2 
 3 import org.springframework.cloud.openfeign.FeignClient;
 4 import org.springframework.stereotype.Service;
 5 import org.springframework.web.bind.annotation.GetMapping;
 6 import org.springframework.web.bind.annotation.RequestParam;
 7 
 8 @Service
 9 @FeignClient("seata-stock")
10 public interface OpenFeignService {
11     @GetMapping("/handleStock")
12     Integer handleStock(@RequestParam("productId") Integer productId);
13 }

  OrderServiceImpl详情如下:

 1 package com.snails.service.impl;
 2 
 3 import com.snails.openfeign.OpenFeignService;
 4 import com.snails.mapper.OrderMapper;
 5 import com.snails.service.OrderService;
 6 import io.seata.spring.annotation.GlobalTransactional;
 7 import org.springframework.stereotype.Service;
 8 import org.springframework.transaction.annotation.Transactional;
 9 import javax.annotation.Resource;
10 
11 @Service
12 public class OrderServiceImpl implements OrderService {
13 
14     @Resource
15     private OpenFeignService openFeignService;
16 
17     @Resource
18     private OrderMapper orderMapper;
19 
20     @Override
21     // 本地事务
22     @Transactional
23     // seata 全局事务
24     @GlobalTransactional
25     public Integer handleOrder(Integer productId) {
26         openFeignService.handleStock(productId);
27 //        int i = 1/0;
28         orderMapper.addOrder(productId,  1);
29         return 1;
30     }
31 }

  OrderController详情如下:

package com.snails.controller;
import com.snails.openfeign.OpenFeignService;
import com.snails.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/handleOrder")
    public Integer handleOrder(Integer productId) {
        orderService.handleOrder(productId);
        return 1;
    }
}

  SeataOrder6021Application详情如下:

 1 package com.snails;
 2 
 3 import org.mybatis.spring.annotation.MapperScan;
 4 import org.springframework.boot.SpringApplication;
 5 import org.springframework.boot.autoconfigure.SpringBootApplication;
 6 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 7 import org.springframework.cloud.openfeign.EnableFeignClients;
 8 
 9 @SpringBootApplication
10 @EnableDiscoveryClient
11 @EnableFeignClients
12 @MapperScan("com.snails.mapper")
13 public class SeataOrder6021Application {
14     public static void main(String[] args) {
15         SpringApplication.run(SeataOrder6021Application.class, args);
16     }
17 }

1.3、启动订单服务

  0

  出现如上信息,订单服务搭建完成。

2.1.2、库存服务

2.1、配置文件信息

  appication.yml:

 1 server:
 2   port: 6061
 3 spring:
 4   application:
 5     name: seata-stock
 6   datasource:
 7     driver-class-name: com.mysql.cj.jdbc.Driver
 8     url: jdbc:mysql://192.168.33.55:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
 9     username: root
10     password: root
11     type: com.alibaba.druid.pool.DruidDataSource
12   cloud:
13     nacos:
14       discovery:
15         server-addr: 192.168.33.55:8848
16     alibaba:
17       seata:
18         tx-service-group: SEATA-GROUP
19 
20 seata:
21   tx-service-group: trxgroup
22   service:
23     vgroup-mapping:
24       trxgroup: default
25   config:
26     # support: nacos, consul, apollo, zk, etcd3
27     type: nacos
28     nacos:
29       server-addr: 192.168.33.55:8848
30       namespace:
31       group: SEATA_GROUP
32       username:
33       password:
34       data-id: seataServer.properties
35   registry:
36     # support: nacos, eureka, redis, zk, consul, etcd3, sofa
37     type: nacos
38     nacos:
39       application: seata-server
40       server-addr: 192.168.33.55:8848
41       group: SEATA_GROUP
42       namespace:
43       cluster: default
44       username: nacos
45       password: nacos
46   store:
47     # support: file 、 db 、 redis
48     mode: db
49     db:
50       datasource: druid
51       db-type: mysql
52       driver-class-name: com.mysql.cj.jdbc.Driver
53       url: jdbc:mysql://192.168.33.55:3306/seata?rewriteBatchedStatements=true
54       user: root
55       password: root
56       min-conn: 5
57       max-conn: 100
58       global-table: global_table
59       branch-table: branch_table
60       lock-table: lock_table
61       distributed-lock-table: distributed_lock
62       query-limit: 100
63       max-wait: 5000
64 #设置feign客户端超时时间(OpenFeign默认支持ribbon)
65 ribbon:
66  # 建立连接后从服务器读取到可用资源所用的时间 6s
67  ReadTimeout: 6000
68  # 建立连接所用的时间   6s
69  ConnectTimeout: 6000

2.2、库存服务业务代码

  StockServiceMapper详情如下:

 1 package com.snails.mapper;
 2 
 3 import org.apache.ibatis.annotations.Mapper;
 4 import org.apache.ibatis.annotations.Param;
 5 import org.apache.ibatis.annotations.Update;
 6 
 7 @Mapper
 8 public interface StockServiceMapper {
 9 
10     @Update("update stock set stock = stock - 1 where product_id = #{productId}")
11     Integer handleStock(@Param("productId") Integer productId);
12 
13 }

  StockService详情如下:

1 package com.snails.service;
2 
3 public interface StockService {
4     Integer handleStock(Integer productId);
5 }

  StockServiceImpl详情如下:

 1 package com.snails.service.impl;
 2 
 3 import com.snails.service.StockService;
 4 import com.snails.mapper.StockServiceMapper;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.stereotype.Service;
 7 
 8 @Service
 9 public class StockServiceImpl implements StockService {
10 
11     @Autowired
12     private StockServiceMapper stockMapper;
13 
14     @Override
15     public Integer handleStock(Integer productId) {
16         return stockMapper.handleStock(productId);
17     }
18 }

  StockController详情如下:

 1 package com.snails.controller;
 2 
 3 import com.snails.service.StockService;
 4 import org.springframework.beans.factory.annotation.Autowired;
 5 import org.springframework.web.bind.annotation.GetMapping;
 6 import org.springframework.web.bind.annotation.RequestParam;
 7 import org.springframework.web.bind.annotation.RestController;
 8 import java.util.Objects;
 9 
10 @RestController
11 public class StockController {
12 
13     @Autowired
14     private StockService stockService;
15 
16     @GetMapping("/handleStock")
17     public Integer handleStock(@RequestParam("productId") Integer productId) {
18         if (Objects.isNull(productId) || productId < 0) {
19             return -1;
20         }
21         return stockService.handleStock(productId);
22     }
23 }

  SeataStock6061Application详情如下:

 1 package com.snails;
 2 
 3 import org.mybatis.spring.annotation.MapperScan;
 4 import org.springframework.boot.SpringApplication;
 5 import org.springframework.boot.autoconfigure.SpringBootApplication;
 6 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 7 
 8 @SpringBootApplication
 9 @EnableDiscoveryClient
10 @MapperScan("com.snails.mapper")
11 public class SeataStock6061Application {
12     public static void main(String[] args) {
13         SpringApplication.run(SeataStock6061Application.class, args);
14     }
15 }

2.3、启动库存服务

0

  出现如上日志信息,库存服务搭建完成。

2.2、分布式事务验证

2.2.1、无异常信息的验证

  在订单服务的OrderServiceImpl中,注释异常代码。

  0

  浏览器访问接口:

  0

  订单表数据:

 0

  库存表数据:

 0

2.2.2、有异常信息的验证

  在订单服务的OrderServiceImpl中,放开异常代码,同时以debug模式启动订单服务,便于观察undo_log表记录。

  0

  undo_log表记录详情如下,回滚信息中记录更新前后数据记录。

  0

  浏览器访问接口:

 0

  库存服务日志详情:

  0

  订单服务日志详情:

  0

2.3、搭建过程中的问题

2.3.1、无法连接到seata-server服务端

  使用sh seata-server.sh 启动seata-server服务端,客户端接入时报错:

 0
 0

  问题原因:

 0    0

解决方案:

  将配置文件做如下调整

 0

  同时将seata-server服务端的application.yml文件中有关seata的注册中心、配置中心配置复制到客户端的application.yml配置文件中,配置详情如下:

seata:
  tx-service-group: trxgroup
  service:
    vgroup-mapping:
      trxgroup: default
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: 192.168.33.55:8848
      namespace:
      group: SEATA_GROUP
      username:
      password:
      data-id: seataServer.properties
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.33.55:8848
      group: SEATA_GROUP
      namespace:
      cluster: default
      username: nacos
      password: nacos
  store:
    # support: file 、 db 、 redis
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.33.55:3306/seata?rewriteBatchedStatements=true
      user: root
      password: root
      min-conn: 5
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 100
      max-wait: 5000

2.3.2、Invalid bound statement (not found)

  页面访问请求:

0

  后台报错:

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):

原因:

  启动类未扫描到Mapper的程序。

代码问题:

  1、OrderMapper随意写在了service包下

  2、启动类中@MapperSacn未指定扫描的包

 0

解决方案:

  1、新建mapper文件夹,将OrderMapper.java移至此文件夹下

  2、启动类的@MapperScan注解中添加需要扫描的包

 0

2.3.3、openFeign访问请求方式问题

问题:

 0

原因:

  在OpenFeign远程调用请求,在Order的OpenFeignService的handleStock方法里,参数未用@RequestParam注解修饰,OpenFeign默认使用Post请求方式,而在库存服务中,StockController中的handleStock方法使用的是GetMapping,即get请求方式。

 0    0

解决方案:

  在方法参数中添加@RequestParam注解。

 0     0

  在使用Seata的AT模式,必须创建undo_log表。如果未创建,在本例中会在库存服务中报错,提示回滚表不存在,报错详情如下:

 0
 
posted @ 2024-02-08 10:20  无虑的小猪  阅读(296)  评论(0编辑  收藏  举报