Loading

Seata-AT模式

1.简介

概念:AT模式是一种无侵入的分布式事务解决方案,在 AT 模式下用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

整体机制:

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
  • 提交异步化,非常快速地完成。
  • 回滚通过一阶段的回滚日志进行反向补偿。

一阶段:

在一阶段中,Seata会拦截“业务SQL“,首先解析SQL语义,找到要更新的业务数据,在数据被更新前,保存下来"undo",然后执行”业务SQL“更新数据,更新之后再次保存数据”redo“,最后生成行锁,这些操作都在本地数据库事务内完成,这样保证了一阶段的原子性。

二阶段:

相对一阶段,二阶段比较简单,负责整体的回滚和提交,如果之前的一阶段中有本地事务没有通过,那么就执行全局回滚,否在执行全局提交,回滚用到的就是一阶段记录的"undo Log",通过回滚记录生成反向更新SQL并执行,以完成分支的回滚。当然事务完成后会释放所有资源和删除所有日志。

image

2.案例

设计2个服务,分别是seata-stock和seata-order

seata-order远程调用seata-stock

  • seata-stock服务:

    数据库:

    CREATE DATABASE `seata-stock` 
    CREATE TABLE `stock` (
    `product_id` int(2) NOT NULL AUTO_INCREMENT,
    `count` int(2) DEFAULT NULL,
    `money` int(2) DEFAULT NULL,
    PRIMARY KEY (`product_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
    
    // seata用于回滚的日志表
    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;
    

    pom.xml

    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
         <version>2.6.4</version>
         <relativePath/> <!-- lookup parent from repository -->
     </parent>
    
     <groupId>com.zt.studydemo</groupId>
     <artifactId>seata-stock</artifactId>
     <version>0.0.1-SNAPSHOT</version>
     <name>seata-stock</name>
     <description>Demo project for Spring Boot</description>
    
     <properties>
         <java.version>1.8</java.version>
     </properties>
    
     <dependencies>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
    
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
    
         <dependency>
             <groupId>org.mybatis.spring.boot</groupId>
             <artifactId>mybatis-spring-boot-starter</artifactId>
             <version>2.2.0</version>
         </dependency>
    
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>druid</artifactId>
             <version>1.2.9</version>
         </dependency>
    
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>8.0.26</version>
         </dependency>
    
         <dependency>
             <groupId>com.alibaba.cloud</groupId>
             <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
         </dependency>
    
         <dependency>
             <groupId>com.alibaba.cloud</groupId>
             <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
         </dependency>
     </dependencies>
    
     <dependencyManagement>
         <dependencies>
             <dependency>
                 <groupId>org.springframework.cloud</groupId>
                 <artifactId>spring-cloud-dependencies</artifactId>
                 <version>2021.0.1</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
    
             <dependency>
                 <groupId>com.alibaba.cloud</groupId>
                 <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                 <version>2.2.7.RELEASE</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
         </dependencies>
     </dependencyManagement>
    
     <build>
         <plugins>
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
         </plugins>
     </build>
    
    </project>
    

    yml:

    server:
    port: 8788
    
    spring:
    main:
     allow-circular-references: true
    application:
     name: seata-stock
    cloud:
     nacos:
       discovery:
         server-addr: 127.0.0.1:8848
     alibaba:
       seata:
         #事务分组,和seata-server中事务分组保持一致
         tx-service-group: mygroup
    datasource:
     type: com.alibaba.druid.pool.DruidDataSource
     url: jdbc:mysql://127.0.0.1:3306/seata-stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
     username: root
     password: root
     driver-class-name: com.mysql.cj.jdbc.Driver
    
    seata:
    #事务分组,和seata-server中事务分组保持一致,如果配置了这个上面的spring.cloud.alibaba.seata.tx-service-group就可以不用配置
    #如果没有配置这个,应用程序会使用上面配置的事务分组来匹配
    tx-service-group: mygroup
    service:
     vgroup-mapping:
       #key是事务组名称 value要和服务端的机房名称保持一致
       mygroup: default
    
    mybatis:
    mapper-locations: classpath:mybatis/*.xml
    type-aliases-package: com.zt.studydemo.seatastock.entity
    
    

    StockMapper:

    public interface StockMapper {
    
    	/**
    	 * 初始化库存
    	 */
    	void initStock();
    
    	/**
         * 减库存操作
    	 */
    	void decrement();
    
    }
    

    StockMapper.xml:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="com.zt.studydemo.seatastock.mapper.StockMapper" >
    
    	<!-- 查询全部数据 -->
    	<insert id="initStock">
    		INSERT INTO stock (`count`, `money`) VALUES ('100','10');
    	</insert>
    
        <!-- 查询全部数据 -->
    	<update id="decrement">
    		update stock set count = count-1
    	</update>
    
    </mapper>
    

    StockService:

    import com.zt.studydemo.seatastock.mapper.StockMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class StockService {
    
        @Autowired
        private StockMapper stockMapper;
    
        /**
         * 初始化库存
         */
        public void initStock(){
            stockMapper.initStock();
        }
    
        /**
         * 减库存操作
         */
        public void decrement(){
            stockMapper.decrement();
        }
    
    }
    

    StockController:

    import com.zt.studydemo.seatastock.service.StockService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class StockController {
    
        @Autowired
        StockService stockService;
    
        @RequestMapping(value="/stock/initStock")
        public String initStock() {
            stockService.initStock();
            return "初始化库存";
        }
    
        @RequestMapping(value="/stock/decrement")
        public String decrement() {
            stockService.decrement();
            return "减库存";
        }
    }
    

    SeataStockApplication:

    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @SpringBootApplication
    @EnableDiscoveryClient
    @MapperScan("com.zt.studydemo.seatastock.mapper")
    public class SeataStockApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SeataStockApplication.class, args);
        }
    
    }
    
  • seata-order服务:

    数据库:

    CREATE DATABASE `seata-order`CREATE TABLE `order` (`product_id` int(11) NOT NULL AUTO_INCREMENT,`count` int(2) DEFAULT NULL,PRIMARY KEY (`product_id`)) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;// seata用于回滚的日志表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;
    

    pom.xml:

    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-parent</artifactId>     <version>2.6.4</version>     <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.zt.studydemo</groupId> <artifactId>seata-order</artifactId> <version>0.0.1-SNAPSHOT</version> <name>seata-order</name> <description>Demo project for Spring Boot</description> <properties>     <java.version>1.8</java.version> </properties> <dependencies>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-web</artifactId>     </dependency>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-test</artifactId>         <scope>test</scope>     </dependency>     <dependency>         <groupId>org.mybatis.spring.boot</groupId>         <artifactId>mybatis-spring-boot-starter</artifactId>         <version>2.2.0</version>     </dependency>     <dependency>         <groupId>com.alibaba</groupId>         <artifactId>druid</artifactId>         <version>1.2.9</version>     </dependency>     <dependency>         <groupId>mysql</groupId>         <artifactId>mysql-connector-java</artifactId>         <version>8.0.26</version>     </dependency>     <dependency>         <groupId>com.alibaba.cloud</groupId>         <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>     </dependency>     <dependency>         <groupId>com.alibaba.cloud</groupId>         <artifactId>spring-cloud-starter-alibaba-seata</artifactId>     </dependency>     <dependency>         <groupId>org.springframework.cloud</groupId>         <artifactId>spring-cloud-starter-openfeign</artifactId>         <exclusions>             <exclusion>                 <groupId>org.springframework.cloud</groupId>                 <artifactId>spring-cloud-openfeign-core</artifactId>             </exclusion>         </exclusions>     </dependency>     <dependency>         <groupId>org.springframework.cloud</groupId>         <artifactId>spring-cloud-openfeign-core</artifactId>         <version>2.2.6.RELEASE</version>     </dependency> </dependencies> <dependencyManagement>     <dependencies>         <dependency>             <groupId>org.springframework.cloud</groupId>             <artifactId>spring-cloud-dependencies</artifactId>             <version>2021.0.1</version>             <type>pom</type>             <scope>import</scope>         </dependency>         <dependency>             <groupId>com.alibaba.cloud</groupId>             <artifactId>spring-cloud-alibaba-dependencies</artifactId>             <version>2.2.7.RELEASE</version>             <type>pom</type>             <scope>import</scope>         </dependency>     </dependencies> </dependencyManagement> <build>     <plugins>         <plugin>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-maven-plugin</artifactId>         </plugin>     </plugins> </build></project>
    

    yml:

    server:port: 8787spring:main: allow-circular-references: trueapplication: name: seata-ordercloud: nacos:   discovery:     server-addr: 127.0.0.1:8848 alibaba:   seata:     #事务分组,和seata-server中事务分组保持一致     tx-service-group: mygroupdatasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/seata-stock?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driverseata:#事务分组,和seata-server中事务分组保持一致,如果配置了这个上面的spring.cloud.alibaba.seata.tx-service-group就可以不用配置#如果没有配置这个,应用程序会使用上面配置的事务分组来匹配tx-service-group: mygroupservice: vgroup-mapping:   #key是事务组名称 value要和服务端的机房名称保持一致   mygroup: defaultmybatis:mapper-locations: classpath:mybatis/*.xmltype-aliases-package: com.zt.studydemo.seataorder.entity
    

    OrderMapper:

    public interface OrderMapper { /**     * 创建订单     */    void createOrder();}
    

    OrderMapper.xml:

    <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.zt.studydemo.seataorder.mapper.OrderMapper" >	<!-- 创建订单 -->	<insert id="createOrder">		INSERT INTO `order` (`count`) VALUES ('1');	</insert></mapper>
    

    OpenFeignStockService:

    import org.springframework.cloud.openfeign.FeignClient;import org.springframework.stereotype.Service;import org.springframework.web.bind.annotation.GetMapping;@Service@FeignClient("seata-stock")public interface OpenFeignStockService {    /**     * 此方法表示远程调用info/{id}接口     */    @GetMapping("stock/decrement")    public String decrement();}
    

    OrderService:

    import com.zt.studydemo.seataorder.mapper.OrderMapper;import com.zt.studydemo.seataorder.openfeign.OpenFeignStockService;import io.seata.spring.annotation.GlobalTransactional;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService {    @Autowired    private OrderMapper orderMapper;    @Autowired    private OpenFeignStockService openFeignStockService;    /**     * 创建订单     */    @GlobalTransactional// 开启分布式事务    public void createOrder(){        // 调用远程服务减库存        openFeignStockService.decrement();        // 异常点        int i = 1/0;        // 创建订单        orderMapper.createOrder();    }}
    

    OrderController:

    import com.zt.studydemo.seataorder.service.OrderService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class OrderController {    @Autowired    OrderService orderService;    @RequestMapping(value="/order/create")    public String orderCreate() {        orderService.createOrder();        return "生成订单";    }}
    

    SeataOrderApplication:

    import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication@EnableDiscoveryClient@EnableFeignClients@MapperScan("com.zt.studydemo.seataorder.mapper")public class SeataOrderApplication {    public static void main(String[] args) {        SpringApplication.run(SeataOrderApplication.class, args);    }}
    
  • 测试:

    启动nacos,seata-server,seata-stock,seata-order

    在OrderService.createOrder异常出设置断点

    访问:http://127.0.0.1:8787/order/create

    执行前库存表:

    image

    执行后库存表:

    image

    执行后undo_log表:

    image

    此时程序往下执行抛出异常,会发现库存表数据回滚,undo_log表中数据被清除

posted @ 2022-05-21 10:40  ZT丶  阅读(157)  评论(0编辑  收藏  举报