Seata-AT模式
1.简介
概念:AT模式是一种无侵入的分布式事务解决方案,在 AT 模式下用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。
整体机制:
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
一阶段:
在一阶段中,Seata会拦截“业务SQL“,首先解析SQL语义,找到要更新的业务数据,在数据被更新前,保存下来"undo",然后执行”业务SQL“更新数据,更新之后再次保存数据”redo“,最后生成行锁,这些操作都在本地数据库事务内完成,这样保证了一阶段的原子性。
二阶段:
相对一阶段,二阶段比较简单,负责整体的回滚和提交,如果之前的一阶段中有本地事务没有通过,那么就执行全局回滚,否在执行全局提交,回滚用到的就是一阶段记录的"undo Log",通过回滚记录生成反向更新SQL并执行,以完成分支的回滚。当然事务完成后会释放所有资源和删除所有日志。
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
执行前库存表:
执行后库存表:
执行后undo_log表:
此时程序往下执行抛出异常,会发现库存表数据回滚,undo_log表中数据被清除