SpringCloudAlibaba Seata分布式事务

分布式事务

事务是数据库的概念,数据库事务(ACID:原子性、一致性、隔离性和持久性);

分布式事务的产生,是由于数据库的拆分和分布式架构(微服务)带来的,在常规情况下,我们在一个进程中操作一个数据库,这属于本地事务,如果在一个进程(java程序)中操作多个数据库,或者在多个进程中操作一个或多个数据库,就产生了分布式事务;

(1)数据库分库分表就产生了分布式事务;

在这里插入图片描述

(2)项目拆分服务化也产生了分布式事务;

在这里插入图片描述

Seata介绍

Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务;

Seata为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案;

目前使用的流行度情况是:AT > TCC > Saga > XA;XA的流行度是我自己编的。。。

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
  • AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
  • TCC模式:最终一致的分阶段事务模式,有业务侵入
  • SAGA模式:长事务模式,有业务侵入

无论哪种方案,都离不开TC,也就是事务的协调者。

我们可以参看seata各公司使用列表:
https://github.com/seata/seata/issues/1246 大部分公司都采用的AT事务模式;

Seata已经在国内很多团队开始落地,其中不乏有大公司;

Github:https://github.com/seata/seata
官网:http://seata.io/
当前最新版本:1.3.0

Seata架构

在Seata的架构中,一共有三个角色:

在这里插入图片描述

  • TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚;

  • TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务;

  • RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交互以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚;

其中TC为单独部署的 Server 服务端,TM和RM为嵌入到应用中的 Client 客户端;

在Seata中,一个分布式事务的生命周期如下:

三个图的出处不一样,结合着看吧,意思都是一个意思
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  1. TM请求TC开启一个全局事务,TC会生成一个XID作为该全局事务的编号,XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起;

  2. RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联;

  3. TM请求TC告诉XID对应的全局事务是进行提交还是回滚;

  4. TC驱动RM将XID对应的自己的本地事务进行提交还是回滚;

TC就是事务管理器,RM就是每个数据库连接(或者说本地事务),TM就是事务的入口(或者说事务的发起方),当然,它本身也是一个RM

每个微服务都需要有一个undo_log表,用于自动补偿,例如一个事务已经提交了,但这时发生了异常,此时该事务已经无法回滚了。
在提交事务之前,会查询一下要执行事务涉及到的表数据,并记录在undo_log表中,如果发生了上面的情况,则将数据自动改回去,而不需要我们敲代码来执行这个操作。

AT模式事务案例

单体应用多数据源分布式事务

在这里插入图片描述

在Spring Boot单体项目中,如果使用了多数据源,就需要考虑多个数据源的数据一致性,即产生了分布式事务的问题,我们采用Seata的AT事务模式来解决该分布式事务问题;

以电商购物下单为例:
在这里插入图片描述

  1. 准备数据库表和数据;

其中每个库中的undo_log表,是 Seata AT模式必须创建的表,主要用于分支事务的回滚;

  1. 开发一个SpringBoot单体应用

注意:以下代码并没有自己跑通过,是视频老师的

  • dynamic-datasource-spring-boot-starter 多数据源
<?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>

    <groupId>com.bjpowernode</groupId>
    <artifactId>29-seata-distributed-transaction</artifactId>
    <version>1.0.0</version>

    <name>29-seata-distributed-transaction</name>
    <description>29-seata-distributed-transaction project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.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-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- mybatis-spring-boot-starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- seata-spring-boot-starter -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

        <!--
            dynamic-datasource-spring-boot-starter动态数据源

            mybatis-plus的作者写的这个

            作用:在一个项目中可以连接多个数据库(多数据源)


        -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!-- nacos-client -->
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>1.3.1</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--mybatis代码自动生成插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <!--配置文件的位置-->
                    <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
                    <!--生成代码过程中是否打印日志-->
                    <verbose>true</verbose>
                    <!--生成时是否覆盖java文件,xml文件总是合并-->
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>

        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>

</project>
# 服务端口号
server.port=8080

# 应用服务名称
spring.application.name=29-seata-distributed-transaction

# 设置默认的数据源或者数据源组,default master
spring.datasource.dynamic.primary=order-ds

# order数据源配置
spring.datasource.dynamic.datasource.order-ds.url=jdbc:mysql://39.99.163.122:3306/orderdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.order-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.order-ds.username=mysql
spring.datasource.dynamic.datasource.order-ds.password=UoT1R8[09/VsfXoO5>6YteB

# product数据源配置
spring.datasource.dynamic.datasource.product-ds.url=jdbc:mysql://39.99.163.122:3306/productdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.product-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.product-ds.username=mysql
spring.datasource.dynamic.datasource.product-ds.password=UoT1R8[09/VsfXoO5>6YteB

# account数据源配置
spring.datasource.dynamic.datasource.account-ds.url=jdbc:mysql://39.99.163.122:3306/accountdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.account-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.account-ds.username=mysql
spring.datasource.dynamic.datasource.account-ds.password=UoT1R8[09/VsfXoO5>6YteB

# 是否启动对seata的集成
spring.datasource.dynamic.seata=true

# Seata应用编号 default ${spring.application.name}
seata.application-id=${spring.application.name}
# Seata事务组编号,用于TC集群名
seata.tx-service-group=${spring.application.name}-group
# 虚拟组和分组的映射
# 29-seata-distributed-transaction-group 对应的就是 Seata事务组编号
#   也可以尝试用 ${seata.tx-service-group} 来获取
seata.service.vgroup-mapping.29-seata-distributed-transaction-group=default
# 这个default 对应的就是 上面那条的 default
seata.service.grouplist.default=192.168.172.128:8091
  • @DS(value = “order-ds”)

动态切换数据源就是依靠这个注解

  • @GlobalTransactional,事务的入口

还可以用在类上,就相当于给类上的每一个方法都加上了@GlobalTransactional

  • @Transactional,事务的分支,这个不用加也可以?有待考证 todo
package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.OrdersMapper;
import com.bjpowernode.model.Orders;
import com.bjpowernode.model.Product;
import com.bjpowernode.service.AccountService;
import com.bjpowernode.service.OrderService;
import com.bjpowernode.service.ProductService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrdersMapper ordersMapper;

    @Autowired
    private AccountService accountService;

    @Autowired
    private ProductService productService;

    @Override
    @DS(value = "order-ds")
    @GlobalTransactional //seata全局事务注解
    public Integer createOrder(Integer userId, Integer productId) throws Exception {
        Integer amount = 1; // 购买数量暂时设置为 1

        log.info("当前 XID: {}", RootContext.getXID());

        // 减库存
        Product product = productService.reduceStock(productId, amount);

        // 减余额
        accountService.reduceBalance(userId, product.getPrice());

        // 下订单
        Orders order = new Orders();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setPayAmount(product.getPrice().multiply(new BigDecimal(amount)));

        ordersMapper.insertSelective(order);

        log.info("下订单: {}", order.getId());

        // 返回订单编号
        return order.getId();
    }
}








package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.ProductMapper;
import com.bjpowernode.model.Product;
import com.bjpowernode.service.ProductService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    @DS(value = "product-ds")
    @Transactional
    public Product reduceStock(Integer productId, Integer amount) throws Exception {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查库存
        Product product = productMapper.selectByPrimaryKey(productId);
        if (product.getStock() < amount) {
            throw new Exception("库存不足");
        }

        // 扣减库存
        int updateCount = productMapper.reduceStock(productId, amount);
        // 扣除成功
        if (updateCount == 0) {
            throw new Exception("库存不足");
        }

        // 扣除成功
        log.info("扣除 {} 库存成功", productId);
        return product;
    }
}







package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.AccountMapper;
import com.bjpowernode.model.Account;
import com.bjpowernode.service.AccountService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    @DS(value = "account-ds")
    @Transactional
    public void reduceBalance(Integer userId, BigDecimal money) throws Exception {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查余额
        Account account = accountMapper.selectAccountByUserId(userId);
        if (account.getBalance().doubleValue() < money.doubleValue()) {
            throw new Exception("余额不足");
        }

        // 扣除余额
        int updateCount = accountMapper.reduceBalance(userId, money);
        // 扣除成功
        if (updateCount == 0) {
            throw new Exception("余额不足");
        }
        log.info("扣除用户 {} 余额成功", userId);
    }
}

测试:http://localhost:8080/order?userId=1&productId=1

微服务的分布式事务

在这里插入图片描述

  • Cloud项目用的依赖和单体项目用的依赖不一样

Cloud用spring-cloud-starter-alibaba-seata

单体项目用seata-spring-boot-starter

  • 配置文件Seata的部分,与单体项目的配置一样,每个微服务都需要配置
  • 使用方面也与单体项目是一样的
<?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>

    <groupId>com.bjpowernode</groupId>
    <artifactId>30-seata-tcc-order-service</artifactId>
    <version>1.0.0</version>

    <name>30-seata-tcc-order-service</name>
    <description>30-seata-tcc-order-service project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--spring-cloud-starter-alibaba-sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- mybatis-spring-boot-starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- spring-cloud-starter-alibaba-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>


        <!--统一通用项目,model类、feign接口-->
        <dependency>
            <groupId>com.bjpowernode</groupId>
            <artifactId>30-seata-tcc-commons</artifactId>
            <version>1.0.0</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- spring-cloud-dependencies -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>

        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>

</project>

微服务的分布式事务二

就是代码不一样,贴一下,万一以后有用

使用seata DataSourceProxy代理自己的数据源

// DataSourceAutoConfiguration springboot默认的数据源配置
// Hikari 是 springboot默认的数据源
@Import({Hikari.class, Tomcat.class, Dbcp2.class, OracleUcp.class, 
Generic.class, DataSourceJmxConfiguration.class})





// DataSourceConfiguration
// springboot将Hikari配置为数据源
@ConditionalOnMissingBean({DataSource.class}) // 容器中没有我们配置的数据源的时候,才会启动这段代码配置来配置Hikari来作为我们的数据源
static class Hikari {
        Hikari() {
        }

        @Bean
        @ConfigurationProperties(
            prefix = "spring.datasource.hikari"
        )
        HikariDataSource dataSource(DataSourceProperties properties) {
            HikariDataSource dataSource = (HikariDataSource)DataSourceConfiguration.createDataSource(properties, HikariDataSource.class);
            if (StringUtils.hasText(properties.getName())) {
                dataSource.setPoolName(properties.getName());
            }

            return dataSource;
        }
    }




// DataSourceConfiguration
// 上面的createDataSource其实就是这么创建的
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
        return properties.initializeDataSourceBuilder().type(type).build();
    }






// 根据以上源码编写自己的数据源配置,
// ware服务也可以把这段代码直接粘到mybatis.config中
package com.atlinxi.gulimall.order.config;

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;

@Configuration
public class MySeataConfig {

    @Autowired
    DataSourceProperties dataSourceProperties;


    /**
     * DataSourceAutoConfiguration
     *
     * @ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class}) 只要有数据源,就开启数据源的自动配置
     * @ConditionalOnMissingBean(
     *     type = {"io.r2dbc.spi.ConnectionFactory"}
     * )
     *
     * 开启DataSourceProperties进行属性绑定,封装了数据源的属性,例如连接地址等
     * @EnableConfigurationProperties({DataSourceProperties.class})
     * @param dataSourceProperties
     * @return
     */
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder()
                .type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())){
            dataSource.setPoolName(dataSourceProperties.getName());
        }

        // 数据源交给seata
        return new DataSourceProxy(dataSource);
    }
}


部署、使用seata完整流程

package com.atlinxi.gulimall.order;

import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;


/**
 * 使用rabbitmq
 *
 * 1. 引入amqp依赖,RabbitAutoConfiguration就会自动生效
 * 2. 给容器中自动配置了
 *      RabbitTemplate,AmqpAdmin,CachingConnectionFactory,RabbitMessagingTemplate
 *
 *      所有属性都是 @ConfigurationProperties(
 *          prefix = "spring.rabbitmq"
 *          )
 *          public class RabbitProperties
 *
 * 3. 给配置文件中配置spring.rabbitmq.xxx
 * 4. @EnableRabbit,开启功能
 * 5. 监听消息,使用@RabbitListener,必须有@EnableRabbit
 *      @RabbitListener:类/方法上(监听哪些队列)
 *      @RabbitHandler:标在方法上(重载区分不同的消息)
 *          不同的消息实际上就是不同的实体类
 *
 *
 *
 *  seata控制分布式事务,
 *  1. 每一个微服务先必须创建undo_log(mysql的表)
 *  2. 安装事务协调器 https://github.com/seata/seata/releases
 *  3. 整合
 *      1. 导入依赖 spring-cloud-starter-alibaba-seata
 *          seata-all:1.3.0(这个版本和事务协调器的版本是一致的)
 *      2. 解压并启动seata-server
 *          registry.conf:注册中心配置
 *              将seata注册到nacos中
 *                  registry.type = "nacos"
 *                  config.type = "file"
 *      启动成功后在nacos的服务列表中就能看到seata-server
 *
 *      3. 所有想要用到分布式事务的微服务使用seata DataSourceProxy代理自己的数据源
 * 			 seata想要控制事务,自己默认的数据源必须让seata代理,seata才能控制事务
 * 
 *      4. 每个微服务,都必须导入registry.conf和file.conf(下面地址有具体内容)
 *              https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata/account-service
 *         registry.conf
 *              registry.type = nacos    seata的注册中心使用nacos
 *              config.type = file      seata的配置使用file
 *         file.conf(gulimall-order是微服务名,其他均为固定写法)
 *              每个事务都要注册到tc中,tc的名字就是当前应用的名字 + 固定写法
 *              service.vgroupMapping.gulimall-order-seata-service-group = "default"
 *
 *      5. 给分布式大事务的入口标注 @GlobalTransactional
 *          每一个远程的小事务用 @Transactional
 */
@EnableRabbit
@SpringBootApplication
@EnableDiscoveryClient
@EnableRedisHttpSession
@EnableFeignClients
@EnableAspectJAutoProxy(exposeProxy = true)
public class GulimallOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallOrderApplication.class, args);
    }

}

AT事务模式分布式事务工作机制

最常用的一种,因为无代码只需简单配置,且性能还行

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。

基本流程图:
在这里插入图片描述

前提
基于支持本地 ACID 事务的关系型数据库;(mysql、oracle)

Java 应用,通过JDBC访问数据库;

整体机制
就是两阶段提交协议的演变:

  • 一阶段:

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

  • 二阶段:

如果没有异常异步化提交,非常快速地完成;
如果有异常回滚通过一阶段的回滚日志进行反向补偿;

AT模式不适用于高并发
AT模式下的事务会加很多锁,加锁之后相当于把并发变成了串行化,所有人都得等待上一个订单下完,再来下订单,那就不适用于高并发了。

下订单使用seata的AT模式适用的场景不适用高并发场景,是适合我们商品服务中的后台保存商品的场景,也远程调用了很多服务,同时并发量不是很大

而我们的下单是高并发场景。

具体举例说明整个AT分支的工作过程

业务表:product

Field	Type	    Key
id	    bigint(20)	PRI
name	varchar(100)	
since	varchar(100)	

AT分支事务的业务逻辑:

update product set name = 'GTS' where name = 'TXC';

一阶段过程

  1. 解析SQL,得到SQL的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息;

  2. 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据;
    select id, name, since from product where name = ‘TXC’;
    得到前镜像:

id	name	since
1	TXC	    2014
  1. 执行业务 SQL:更新这条记录的 name 为 ‘GTS’;
  2. 查询后镜像:根据前镜像的结果,通过 主键 定位数据;
select id, name, since from product where id = 1;

得到后镜像:

id	name	since
1	GTS	    2014
  1. 插入回滚日志:把前后镜像数据以及业务SQL相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中;

  2. 分支事务提交前,向TC注册分支,申请product表中,主键值等于1的记录的全局锁(在当前的同一个全局事务id范围内是可以申请到全局锁的,不同的全局事务id才会排斥);

  3. 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交;

  4. 将本地事务提交的结果上报给TC;

二阶段-回滚

  1. 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作;
  2. 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录;
  3. 数据校验:拿 UNDO LOG 中的后镜像与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改,这种情况,需要人工来处理;
  4. 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
  1. 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC;

二阶段-提交

  1. 收到TC的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给TC;
  2. 异步任务阶段的分支提交请求将异步和批量地删除相应UNDO LOG记录;

回滚日志表:

Field	        Type
branch_id	    bigint       PK
xid	            varchar(100)
context	        varchar(128)
rollback_info	longblob
log_status	    tinyint
log_created	    datetime
log_modified	datetime
SQL建表语句:
CREATE TABLE `undo_log` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int 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=4 DEFAULT CHARSET=utf8;

写隔离

多线程模式下的一阶段和二阶段是怎么保证数据的一致性的问题的?

一阶段本地事务提交前,需要确保先拿到全局锁(暂时理解为就相当于是个分布式锁)
拿不到 全局锁 ,不能提交本地事务;
拿 全局锁 的尝试被限制在一定范围内(10次),超出范围将放弃,并回滚本地事务,释放本地锁;

以一个示例来说明:

两个或者多个全局事务 tx1 和 tx2,分别并发对 a 表的 m 字段进行更新操作,m 的初始值 1000;

假设tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900,本地事务提交前,先拿到该记录的 全局锁 ,拿到了全局锁,本地提交并释放本地锁;

tx2后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800,本地事务提交前,尝试拿该记录的 全局锁 ,tx1全局提交前,该记录的全局锁一直会被 tx1 持有,tx2 需要重试等待 全局锁 ;

todo 这个图我看的有点儿迷,先放这儿再说吧
在这里插入图片描述

tx1 二阶段全局提交,释放 全局锁 ,tx2 拿到 全局锁 提交本地事务;
在这里插入图片描述

  • 这里的本地锁就是行锁,这里默认两个事务操作的是同一条数据

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚;
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功;

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题;

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted);表示有可能读到未提交的数据(脏读)

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理;

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试,这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回;

如果不加 FOR UPDATE是不需要获取全局锁就可以读到的。

出于总体性能上的考虑,Seata目前的方案并没有对所有SELECT语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句;

TC Server运行环境部署

单机版

我们先部署单机环境的 Seata TC Server,用于学习或测试,在生产环境中要部署集群环境;

因为TC需要进行全局事务和分支事务的记录,所以需要对应的存储,目前,TC有三种存储模式( store.mode ):

  • file模式:适合单机模式,全局事务会话信息在内存中读写,并持久化本地文件 root.data,性能较高;
  • db模式:适合集群模式,全局事务会话信息通过 db 共享,相对性能差点;
  • redis模式:解决db存储的性能问题;

我们先采用file模式,最终我们部署单机TC Server如下图所示:

在这里插入图片描述

Seata运行环境部署
下载Seata:http://seata.io/zh-cn/blog/download.html
解压:tar -zxvf seata-server-1.3.0.tar.gz
切换cd seata

在这里插入图片描述

默认seata-server.sh脚本设置的jvm内存参数2G,我们再虚拟机里面做实验,可以改小一点;

在这里插入图片描述

在bin目录下启动:./seata-server.sh
默认配置下,Seata TC Server 启动在 8091 端口;
因为我们没有修改任何配置文件,默认情况seata使用的是file模式进行数据持久化,所以可以看到用于持久化的本地文件 root.data;

集群版

生产环境下,需要部署集群 Seata TC Server,实现高可用,在集群时,多个 Seata TC Server 通过 db 数据库或者redis实现全局事务会话信息的共享;

每个Seata TC Server注册自己到注册中心上,应用从注册中心获得Seata TC Server实例,这就是Seata TC Server的集群;
在这里插入图片描述

Seata TC Server 对主流的注册中心都提供了集成,Naco作为注册中心越来越流行,这里我们就采用Nacos;

Seata TC Server集群搭建具体步骤
1、下载并解压两个seata-server-1.3.0.tar.gz;
2、初始化 Seata TC Server 的 db 数据库,在 MySQL 中,创建 seata 数据库,并在该库下执行如下SQL脚本:
使用seata-1.3.0\script\server\db脚本(网盘有共享)
3、修改 seata/conf/file.conf 配置文件,修改使用 db 数据库,实现 Seata TC Server 的全局事务会话信息的共享;
(1)mode = “db”
(2)数据库的连接信息
driverClassName = “com.mysql.cj.jdbc.Driver”
url = “jdbc:mysql://39.99.163.122:3306/seata”
user = “mysql”
password = “UoT1R8[09/VsfXoO5>6YteB”
4、设置使用 Nacos 注册中心;
修改 seata/conf/registry.conf 配置文件,设置使用 Nacos 注册中心;
(1)、type = “nacos”
(2)Nacos连接信息:
nacos {
application = “seata-server”
serverAddr = “127.0.0.1:8848”
group = “SEATA_GROUP”
namespace = “”
cluster = “default”
username = “”
password = “”
}
5、启动数据库和nacos;
6、启动两个 TC Server
执行 ./seata-server.sh -p 18091 -n 1 命令,启动第一个TC Server;

  • -p:Seata TC Server 监听的端口;
  • -n:Server node,在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突;

执行 ./seata-server.sh -p 28091 -n 2 命令,启动第二个TC Server;
7、打开Nacos注册中心控制台,可以看到有两个Seata TC Server 实例;
8、应用测试;

在这里插入图片描述

测试

对于SpringBoot单体应用
1、添加nacos客户端依赖;

<!-- nacos-client -->
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>1.3.1</version>
</dependency>

2、配置application.properties文件

# Seata应用编号,默认为${spring.application.name}
seata.application-id=springcloud-order-seata
# Seata事务组编号,用于TC集群名
seata.tx-service-group=springcloud-order-seata-group
# 虚拟组和分组的映射
seata.service.vgroup-mapping.springcloud-order-seata-group=default

#seata-spring-boot-starter 1.1版本少一些配置项
seata.enabled=true
seata.registry.type=nacos
seata.registry.nacos.cluster=default
seata.registry.nacos.server-addr=192.168.172.128:8848
# nacos中Seata服务的分组
seata.registry.nacos.group=SEATA_GROUP
# nacos中Seata服务的名字
seata.registry.nacos.application=seata-server

在这里插入图片描述

在这里插入图片描述

微服务

对于Spring Cloud Alibaba微服务应用:
则不需要加nacos的jar包依赖,application.properties文件配置完全一样;

TCC事务模式执行机制

AT模式基本上能满足我们使用分布式事务大部分需求,但涉及非关系型数据库与中间件的操作、跨公司服务的调用、跨语言的应用调用就需要结合TCC模式;
在这里插入图片描述

一个分布式的全局事务,整体是两阶段提交(Try - [Comfirm/Cancel])的模型,在Seata中,AT模式与TCC模式事实上都是基于两阶段提交,它们的区别在于:

  • AT模式基于支持本地ACID事务的关系型数据库:
  1. 一阶段prepare行为:在本地事务中,一并提交“业务数据更新“和”相应回滚日志记录”;
  2. 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志;
  3. 阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚;
  • 而TCC 模式,需要我们人为编写代码实现提交和回滚:
  1. 一阶段 prepare 行为:调用自定义的 prepare 逻辑;(真正要做的事情,比如插入订单,更新库存,更新余额)
  2. 二阶段 commit 行为:调用自定义的 commit 逻辑;(自己写代码实现)
  3. 二阶段 rollback 行为:调用自定义的 rollback 逻辑;(自己写代码实现)

所以TCC模式,就是把自定义的分支事务的提交和回滚并纳入到全局事务管理中;

通俗来说,Seata的TCC模式就是手工版本的AT模式,它允许你自定义两阶段的处理逻辑而不需要依赖AT模式的undo_log回滚表;

基于SpringBoot单体应用的TCC事务

在这里插入图片描述

  • @LocalTCC

该注解标识此TCC为本地模式,即该事务是本地调用,非RPC调用,@LocalTCC一定需要注解在接口上,此接口可以是寻常的业务接口,只要实现了TCC的两阶段提交对应方法即可;

  • @TwoPhaseBusinessAction

该注解标识为TCC模式,注解try方法,其中name为当前tcc方法的bean名称,写方法名便可(全局唯一),commitMethod指提交方法,rollbackMethod指事务回滚方法,指定好三个方法之后,Seata会根据事务的成功或失败,通过动态代理去帮我们自动调用提交或者回滚;

  • @BusinessActionContextParameter

该注解可以将参数传递到二阶段(commitMethod/rollbackMethod)的方法;BusinessActionContext 是指TCC事务上下文,携带了业务方法的参数;

  • commitMethod可以是空确认,返回true即可
package com.bjpowernode.service;

public interface OrderService {

    /**
     * 下订单
     *
     * @param userId 用户id
     * @param productId 产品id
     * @return 订单id
     * @throws Exception 创建订单失败,抛出异常
     */
    Integer createOrder(Integer userId, Integer productId) throws Exception;

}








package com.bjpowernode.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

import java.math.BigDecimal;

@LocalTCC
public interface AccountService {

    /**
     * 扣除余额
     * 定义两阶段提交
     * name = reduceStock为一阶段try方法
     * commitMethod = commitTcc 为二阶段确认方法
     * rollbackMethod = cancel 为二阶段取消方法
     * BusinessActionContextParameter注解 可传递参数到二阶段方法
     *
     * @param userId 用户ID
     * @param money  扣减金额
     * @throws Exception 失败时抛出异常
     */
    @TwoPhaseBusinessAction(name = "reduceBalance", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
    void reduceBalance(@BusinessActionContextParameter(paramName = "userId") Integer userId,
                       @BusinessActionContextParameter(paramName = "money") BigDecimal money);

    /**
     * 确认方法、可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param context 上下文
     * @return boolean
     */
    boolean commitTcc(BusinessActionContext context);

    /**
     * 二阶段取消方法
     *
     * @param context 上下文
     * @return boolean
     */
    boolean cancelTcc(BusinessActionContext context);

}






package com.bjpowernode.service;

import com.bjpowernode.model.Product;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface ProductService {

    /**
     * 减库存
     *
     * 定义两阶段提交
     * name = reduceStock为一阶段try方法
     * commitMethod = commitTcc 为二阶段确认方法
     * rollbackMethod = cancel 为二阶段取消方法
     * BusinessActionContextParameter注解 可传递参数到二阶段方法
     *
     * @param productId 商品ID
     * @param amount    扣减数量
     * @throws Exception 扣减失败时抛出异常
     */
    @TwoPhaseBusinessAction(name = "reduceStock", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
    Product reduceStock(@BusinessActionContextParameter(paramName = "productId") Integer productId,
                        @BusinessActionContextParameter(paramName = "amount") Integer amount);

    /**
     * 二阶段提交方法
     *
     * 确认方法、可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param context 上下文
     * @return boolean
     */
    boolean commitTcc(BusinessActionContext context);

    /**
     * 二阶段回滚方法
     *
     * @param context 上下文
     * @return boolean
     */
    boolean cancelTcc(BusinessActionContext context);

}
package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.OrdersMapper;
import com.bjpowernode.model.Orders;
import com.bjpowernode.model.Product;
import com.bjpowernode.service.AccountService;
import com.bjpowernode.service.OrderService;
import com.bjpowernode.service.ProductService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrdersMapper ordersMapper;

    @Autowired
    private AccountService accountService;

    @Autowired
    private ProductService productService;

    @Override
    @DS(value = "order-ds")
    @GlobalTransactional //seata全局事务注解, TM 事务发起方
    public Integer createOrder(Integer userId, Integer productId) throws Exception {
        Integer amount = 1; // 购买数量暂时设置为 1

        log.info("当前 XID: {}", RootContext.getXID());

        // 减库存
        Product product = productService.reduceStock(productId, amount);

        // 减余额
        accountService.reduceBalance(userId, product.getPrice());

        // 下订单
        Orders order = new Orders();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setPayAmount(product.getPrice().multiply(new BigDecimal(amount)));

        ordersMapper.insertSelective(order);

        log.info("下订单: {}", order.getId());

        int a = 10 / 0;

        // 返回订单编号
        return order.getId();
    }
}







package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.ProductMapper;
import com.bjpowernode.model.Product;
import com.bjpowernode.service.AccountService;
import com.bjpowernode.service.ProductService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private AccountService accountService;

    @Autowired
    private ProductMapper productMapper;

    @DS(value = "product-ds")
    @Override
    public Product reduceStock(Integer productId, Integer amount) {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查库存
        Product product = productMapper.selectByPrimaryKey(productId);
        if (product.getStock() < amount) {
            throw new RuntimeException("库存不足");
        }

        // 扣减库存
        int updateCount = productMapper.reduceStock(productId, amount);
        // 扣除成功
        if (updateCount == 0) {
            throw new RuntimeException("库存不足");
        }

        // 扣除成功
        log.info("扣除 {} 库存成功", productId);

        return product;
    }

    /**
     * tcc服务(confirm)方法
     * 可以空确认
     *
     * @param context 上下文
     * @return boolean
     */
    @DS(value = "product-ds")
    @Override
    public boolean commitTcc(BusinessActionContext context) {
        log.info("Confirm阶段,ProductServiceImpl, commitTcc --> xid = " + context.getXid() + ", commitTcc提交成功");
        return true;
    }

    /**
     * tcc服务(cancel)方法
     *
     * @param context 上下文
     * @return boolean
     */
    @DS(value = "product-ds")
    @Override
    public boolean cancelTcc(BusinessActionContext context) {
        log.info("Cancel阶段,ProductServiceImpl, cancelTcc --> xid = " + context.getXid() + ", cancelTcc提交失败");

        //TODO 这里可以实现中间件、非关系型数据库的回滚操作
        log.info("Cancel阶段,ProductServiceImpl, cancelTcc this data: {}, {}", context.getActionContext("productId"), context.getActionContext("amount"));

        //进行数据库回滚处理
        Integer productId = (Integer)context.getActionContext("productId");
        Integer amount = (Integer)context.getActionContext("amount");
        //把库存再加回去 (避免数据出问题,加个锁,分布式环境下就需要分布式锁)
        productMapper.increaseStock(productId, amount);

        return true;
    }
}





package com.bjpowernode.service.impl;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.bjpowernode.mapper.AccountMapper;
import com.bjpowernode.model.Account;
import com.bjpowernode.service.AccountService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @DS(value = "account-ds")
    @Override
    public void reduceBalance(Integer userId, BigDecimal money) {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查余额
        Account account = accountMapper.selectAccountByUserId(userId);
        if (account.getBalance().doubleValue() < money.doubleValue()) {
            throw new RuntimeException("余额不足");
        }

        // 扣除余额
        int updateCount = accountMapper.reduceBalance(userId, money);
        // 扣除成功
        if (updateCount == 0) {
            throw new RuntimeException("余额不足");
        }
        log.info("扣除用户 {} 余额成功", userId);

        //int a = 10 / 0;
    }

    /**
     * tcc服务(confirm)方法
     * 可以空确认
     *
     * @param context 上下文
     * @return boolean
     */
    @DS(value = "account-ds")
    @Override
    public boolean commitTcc(BusinessActionContext context) {
        log.info("Confirm阶段,AccountServiceImpl, commitTcc --> xid = {}", context.getXid() + ", commitTcc提交成功");
        return true;
    }

    /**
     * tcc服务(cancel)方法
     *
     * @param context 上下文
     * @return boolean
     */
    @DS(value = "account-ds")
    @Override
    public boolean cancelTcc(BusinessActionContext context) {
        log.info("Cancel阶段,AccountServiceImpl, cancelTcc --> xid = " + context.getXid() + ", cancelTcc提交失败");
        //TODO 这里可以实现中间件、非关系型数据库的回滚操作
        log.info("Cancel阶段,AccountServiceImpl, cancelTcc this data: userId= {}, money = {}", context.getActionContext("userId"), context.getActionContext("money"));

        //进行数据库回滚处理
        Integer userId = (Integer)context.getActionContext("userId");
        BigDecimal money = (BigDecimal)context.getActionContext("money");

        //把余额再加回去
        accountMapper.increaseBalance(userId, money);

        return true;
    }
}

基于Spring Cloud Alibaba的TCC分布式事务

具体代码实现和springboot单体应用的代码实现几乎没有区别,具体参考Git上提交的代码;

在这里插入图片描述

XA模式

少用,因为性能太差了

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。

两阶段提交

XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。

正常情况:
在这里插入图片描述
异常情况:

在这里插入图片描述

一阶段:

  • 事务协调者通知每个事物参与者执行本地事务
  • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

二阶段:

  • 事务协调者基于一阶段的报告来判断下一步操作
  • 如果一阶段都成功,则通知所有事务参与者,提交事务
  • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

Seata的XA模型

Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:

在这里插入图片描述
RM一阶段的工作:

  • ① 注册分支事务到TC

  • ② 执行分支业务sql但不提交

  • ③ 报告执行状态到TC

TC二阶段的工作:

TC检测各分支事务执行状态

  • 如果都成功,通知所有RM提交事务
  • 如果有失败,通知所有RM回滚事务

RM二阶段的工作:

  • 接收TC指令,提交或回滚事务

优缺点
XA模式的优点是什么?

  • 事务的强一致性,满足ACID原则。
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

部分内容转发自:
https://www.bilibili.com/video/BV1vy4y167Dc/?p=219&spm_id_from=pageDriver&vd_source=64c73c596c59837e620fed47fa27ada7
https://cloud.tencent.com/developer/article/2236312

华为公司一直贯彻选拔制,因为人才不是靠培养,而是自我成长,我们要创造人才成长的土壤,就如“一杯咖啡吸收宇宙能量”。我们不要给高级专家担负太多管理人才的责任,不要搞“拉郎配”,要给他们自由度,他有多大能量就发挥多大能量。

任正非

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值