四、Seata的AT模式

Seata的AT模式

概念

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

前提

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制

两阶段提交协议的演变:

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

一阶段

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

二阶段

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

图解

在这里插入图片描述

具体实现

实现代码

实现代码地址:https://download.csdn.net/download/m_lonel/85911509

  1. 首先新建两个服务,一个订单服务seata-order-8001, 一个库存服务seata-stock-8002

在这里插入图片描述

需要引入OpenFeign和Mybatis等相关依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.1.1</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.27</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.11</version>
</dependency>

<!--  spring-cloud-loadbalancer-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-loadbalancer</artifactId>
    <version>3.1.1</version>
</dependency>

seata-order-8001服务:

  1. pom文件
<?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.example</groupId>
        <artifactId>SeataTestProject</artifactId>
        <version>1.0-SNAPSHOT</version>
<!--        <relativePath/> &lt;!&ndash; lookup parent from repository &ndash;&gt;-->
    </parent>
    <groupId>com.seata</groupId>
    <artifactId>seata-order-8001</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>seata-order-8001</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>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- Seata依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

    </dependencies>

</project>

  1. 启动类需要加上注解@EnableDiscoveryClient和@EnableFeignClients

在这里插入图片描述

  1. 配置文件application.yml

    server:
      port: 8001
    spring:
      application:
        name: seata-order
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        alibaba:
          seata:
            tx-service-group: default_tx_group   # 事务组名称
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata-order?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
        username: root
        password: 123456
        type: com.alibaba.druid.pool.DruidDataSource
    
    seata:
      tx-service-group: default_tx_group # 事务组名称,要和服务端对应(Nacos中的配置)
      service:
        vgroup-mapping:
          default_tx_group: default # key是事务组名称 value要和服务端的机房名称保持一致
      config:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          group: "SEATA_GROUP"
          namespace: ""
          dataId: "seataServer.properties"
          username: "nacos"
          password: "nacos"
      registry:
        type: nacos
        nacos:
          application: seata-server
          server-addr: 127.0.0.1:8848
          group: "SEATA_GROUP"
          namespace: ""
          username: "nacos"
          password: "nacos"
    

    注意:配置项tx-service-group需对应服务端指的是Nacos中的配置,因为在上一章节,我们把整个Seata服务端相对应的所有配置都上传到了Nacos中

在这里插入图片描述

注意这个配置:default_tx_group: default # key是事务组名称 value要和服务端的机房名称保持一致

这个配置的defaut,也需要和Nacos中的配置保持对应,同时还需要和seata-server的conf文件夹中的registry.conf文件中的配置保持一致

在这里插入图片描述
在这里插入图片描述

  1. 代码
package com.seata.seataOrder8001.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PutMapping;

import java.util.Map;

@FeignClient("seata-stock")
public interface StockClient {

    @PutMapping("/changeStock-at")
    String changeStockAt();

}
package com.seata.seataOrder8001.controller;

import com.seata.seataOrder8001.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/createOrder-at")
    @GlobalTransactional// 开启分布式事务
    public String createAt(){
        orderService.createAt();
        return "生成订单";
    }

}
package com.seata.seataOrder8001.service;

public interface OrderService {

    /**
     * AT
     */
    void createAt();
}
package com.seata.seataOrder8001.service.impl;

import com.seata.seataOrder8001.client.StockClient;
import com.seata.seataOrder8001.mapper.OrderMapper;
import com.seata.seataOrder8001.service.OrderService;
import io.seata.core.context.RootContext;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Map;

@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private StockClient stockClient;

    @Override
    public void createAt() {
        System.out.println("AT------------------> xid = " + RootContext.getXID());

        // 减库存
        stockClient.changeStock();

        // 添加异常, 测试时此处添加断点
        int i = 1/0;

        // 创建订单
        orderMapper.create();
    }

}

package com.seata.seataOrder8001.mapper;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper {

    @Insert("insert into tb_order (count) values (1)")
    void create();

}

seata-stock-8002服务:

  1. pom文件
<?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.example</groupId>
        <artifactId>SeataTestProject</artifactId>
        <version>1.0-SNAPSHOT</version>
<!--        <relativePath/> &lt;!&ndash; lookup parent from repository &ndash;&gt;-->
    </parent>
    <groupId>com.seata</groupId>
    <artifactId>seata-stock-8002</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>seata-stock-8002</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>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- Seata依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
    </dependencies>

</project>

  1. 启动类需要加上注解@EnableDiscoveryClient

在这里插入图片描述

  1. 配置文件application.yml
server:
  port: 8002
spring:
  application:
    name: seata-stock
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    alibaba:
      seata:
        tx-service-group: default_tx_group   # 事务组名称
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata-stock?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource

seata:
  tx-service-group: default_tx_group # 事务组名称,要和服务端对应
  service:
    vgroup-mapping:
      default_tx_group: default # key是事务组名称 value要和服务端的机房名称保持一致
      disable-global-transaction: false
  seata:
    registry:
      type: nacos
      nacos:
        application: seata-server
        server-addr: 127.0.0.1:8848
        group: "SEATA_GROUP"
        namespace: ""
        username: "nacos"
        password: "nacos"
  1. 代码
package com.seata.seataStock8002.controller;

import com.seata.seataStock8002.service.StockService;
import com.seata.seataStock8002.service.StockServiceTcc;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Map;

@RestController
public class StockController {

    @Resource
    private StockService stockService;

    @Resource
    private StockServiceTcc stockServiceTcc;

    @PutMapping("/changeStock-at")
    public String changeStockAt() {
        stockService.changeStockAt();
        return "库存减1";
    }
}
package com.seata.seataStock8002.service;

public interface StockService {

    void changeStockAt();
}

package com.seata.seataStock8002.service.impl;

import com.seata.seataStock8002.mapper.StockMapper;
import com.seata.seataStock8002.service.StockService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class StockServiceImpl implements StockService {

    @Resource
    private StockMapper stockMapper;

    @Override
    public void changeStockAt() {
        stockMapper.subStock();
    }
}

package com.seata.seataStock8002.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;

@Mapper
public interface StockMapper {

    @Update("update tb_stock set count = count - 1 where goods_id = 1")
    void subStock();
}

数据库设计

  1. 新建两个数据库:seata-order和seata-stock

在这里插入图片描述

seata-order数据库中的表为:

CREATE TABLE `tb_order`  (
  `goods_id` bigint NOT NULL AUTO_INCREMENT,
  `count` int NULL DEFAULT NULL,
  PRIMARY KEY (`goods_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;

-- 注意此处0.7.0+ 增加字段 context,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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

seata-stock数据库中的表为:

CREATE TABLE `tb_stock`  (
  `goods_id` bigint NOT NULL,
  `count` int NULL DEFAULT NULL,
  `money` decimal(11, 2) NULL DEFAULT NULL,
  PRIMARY KEY (`goods_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '库存表' ROW_FORMAT = Dynamic;

-- 注意此处0.7.0+ 增加字段 context, 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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- 数据
INSERT INTO `tb_stock` VALUES (1, 100, 50.00);

具体操作

  1. 首先,先将Nacos 运行起来,再将Seata-Server运行起来

    Nacos启动命令(standalone代表着单机模式运行,非集群模式):

    startup.cmd -m standalone
    

    在这里插入图片描述

    启动Seata-Server的方式非常简单,直接双击此文件即可: seata-server-1.4.2\bin\seata-server.bat

在这里插入图片描述

  1. 然后,将服务seata-order-8001和 seata-stock-8002运行起来,跑起来后可以在Seata-Server上看到

在这里插入图片描述

  1. 这个时候我们进行访问Order的REST接口:http://localhost:8001/createOrder-at,我们就会发现此时已经解决了分布式事务问题,远程调用失败,seata-stock库中的库存表tb_stock的库存数量count没有减少,seata-order库中的订单表tb_order中订单记录也没有新增

    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  1. 此时我们用debug模式将8001服务跑起来, 然后访问接口:http://localhost:8001/createOrder-at,程序会卡在断点处,此时我们来查看seata-stock库中的undo_log表和tb_stock表,此时我们会发现,库存表tb_stock的库存数量count确实减少了,但是在undo_log表中出现了快照,记录了当前修改前的数据,这个数据就是用于回滚的数据

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  1. 此时我们放行断点,发现seata-stock库中库存表tb_stock的库存数量count恢复为100了,回滚生效,undo_log表中用于回滚的临时数据也会被删除

    在这里插入图片描述
    在这里插入图片描述
    seata-order库中的订单表tb_order中订单记录也没有新增,此时我们就验证了AT事务的执行过程
    在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

竹峰的风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值