分布式事务
事务是数据库的概念,数据库事务(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模式事务案例
注意
在一次面试的过程中,面试官说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