分布式事务
事务是数据库的概念,数据库事务(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中,一个分布式事务的生命周期如下:
三个图的出处不一样,结合着看吧,意思都是一个意思
-
TM请求TC开启一个全局事务,TC会生成一个XID作为该全局事务的编号,XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起;
-
RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联;
-
TM请求TC告诉XID对应的全局事务是进行提交还是回滚;
-
TC驱动RM将XID对应的自己的本地事务进行提交还是回滚;
TC就是事务管理器,RM就是每个数据库连接(或者说本地事务),TM就是事务的入口(或者说事务的发起方),当然,它本身也是一个RM
每个微服务都需要有一个undo_log
表,用于自动补偿
,例如一个事务已经提交了,但这时发生了异常,此时该事务已经无法回滚了。
在提交事务之前,会查询一下要执行事务涉及到的表数据,并记录在undo_log
表中,如果发生了上面的情况,则将数据自动改回去,而不需要我们敲代码来执行这个操作。
AT模式事务案例
单体应用多数据源分布式事务
在Spring Boot单体项目中,如果使用了多数据源,就需要考虑多个数据源的数据一致性,即产生了分布式事务的问题,我们采用Seata的AT事务模式来解决该分布式事务问题;
以电商购物下单为例:
- 准备数据库表和数据;
其中每个库中的undo_log
表,是 Seata AT模式必须创建的表,主要用于分支事务的回滚;
- 开发一个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';
一阶段过程
-
解析SQL,得到SQL的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息;
-
查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据;
select id, name, since from product where name = ‘TXC’;
得到前镜像:
id name since
1 TXC 2014
- 执行业务 SQL:更新这条记录的 name 为 ‘GTS’;
- 查询后镜像:根据前镜像的结果,通过 主键 定位数据;
select id, name, since from product where id = 1;
得到后镜像:
id name since
1 GTS 2014
-
插入回滚日志:把前后镜像数据以及业务SQL相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中;
-
分支事务提交前,向TC注册分支,申请product表中,主键值等于1的记录的全局锁(在当前的同一个全局事务id范围内是可以申请到全局锁的,不同的全局事务id才会排斥);
-
本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交;
-
将本地事务提交的结果上报给TC;
二阶段-回滚
- 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作;
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录;
- 数据校验:拿 UNDO LOG 中的后镜像与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改,这种情况,需要人工来处理;
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
- 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC;
二阶段-提交
- 收到TC的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给TC;
- 异步任务阶段的分支提交请求将异步和批量地删除相应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事务的关系型数据库:
- 一阶段prepare行为:在本地事务中,一并提交“业务数据更新“和”相应回滚日志记录”;
- 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志;
- 阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚;
- 而TCC 模式,需要我们人为编写代码实现提交和回滚:
- 一阶段 prepare 行为:调用自定义的 prepare 逻辑;(真正要做的事情,比如插入订单,更新库存,更新余额)
- 二阶段 commit 行为:调用自定义的 commit 逻辑;(自己写代码实现)
- 二阶段 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
华为公司一直贯彻选拔制,因为人才不是靠培养,而是自我成长,我们要创造人才成长的土壤,就如“一杯咖啡吸收宇宙能量”。我们不要给高级专家担负太多管理人才的责任,不要搞“拉郎配”,要给他们自由度,他有多大能量就发挥多大能量。
任正非