事务概述
数据库事务
-
A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部
分成功部分失败的情况。 -
C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。
比如:张三向李四转 100 元,转账前和转账后的数据是正确状态这叫一致性,如果出现张三
转出 100 元,李四账户没有增加 100 元这就出现了数据错误,就没有达到一致性。 -
I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互
不干扰,一个事务不能看到其他事务的运行过程的中间状态。通过配置事务隔离级别可以比避免脏
读、重复读问题。 -
D(Durability):持久性,事务完成之后,该事务对数据的更改会持久到数据库,且不会被回
滚。
分布式事务
分布式事务:分布式+事务
一句话理解:订单生成业务:
1.订单生成
2.扣减库存
这是两个系统 并且是两个业务数据库 这就涉及到了分布式事务 如何控制?
seata
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式决方案。一句话理解 加一个注解就能解决分布式事务
seata术语
-
TC:事务协调者 维护全局和分支事务的状态。独立的中间件 需要单独部署。
-
TM:事务管理器 开启全局事务、提交或回滚 发起者
-
RM:资源管理器 管理分支事务负责注册汇报等各分支的情况,驱动分支事务的提交和回滚
分布式事物的几种模式
XA模式
典型的2pc模式 执行原理
1.TM向TC注册一个全局事务
2.根据业务 各个RM会逐个向TC注册分支事务
3.各个RM在接到指令后会在本地进行事务预执行
4.然后各个RM将执行结果报告的TC 这个结果可能成功也可能失败
5.TC在收到各个RM的报告后会获取汇总结果并将总结果汇报给TM,根据汇总结果TM会向TC发起最后确认指令
6.TC收到最后执行向各个RM发送最终执行指令— 提交还是回滚
两阶段提交,将整个事务流程分为两个阶段,准备阶段、提交阶段,2是两个阶段,P是准备阶段,C是提交阶段
举个例子:我和你相约饭店吃饭,咱们使用AA制付款,只有我们都付了钱,老板才会上菜。如果有一个人没付款或不够钱付款,那么将不上菜并且会确保收了部分的钱退还。
准备阶段:老板要求咱两付钱,我付钱,你也付钱
提交阶段:老板确认完毕,开始做菜,坐等吃饭
异常:其中有人没付钱或者没菜了,不会上菜或者会退款给咱们。
AT模式
seata默认使用AT协议
和XA协议非常相似 区别点在于 在所有的RM执行完毕第二阶段的Global commit 后 AT模式能够自动异步方式批量清理掉回滚日志。而XA模式不会自动清洗,需要手动清理
2PC
优点:原理和实现都比较简单
缺陷:同步阻塞、存在中心化问题 实现太过于保守。
Saga模式
Saga模式是1987年由普林斯顿大学的Hector Garcia-Molina和Kenneth Salem共同提出的。该模式不同于前面的模式,它不是2PC的,其是一种长事务解决方案。
其应用场景是:
在无法提供TC、TM、RM接口的情况下,对于一个流程很长的复杂业务,其会包含很多的子流程(事
务)。每个子流程都让它们真实提交它们真正的执行结果。只有当前子流程执行成功后才能执行下一个子流程。若整个流程中所有子流程全部执行成功,则整个业务流程成功;只要有一个子流程执行失败,
则可采用两种补偿方式:
向后恢复:对于其前面所有成功的子流程,其执行结果全部撤销
向前恢复:重试失败的子流程直到其成功
seata-server配置和启动
官网:https://seata.io/zh-cn/blog/download.html 源码包和运行包都要下载 因为包含了很多脚本
下载源码包和运行包 注意:需要注意使用seata和springcloud兼容的版本 https://github.com/alibaba/spring-cloud-alibaba/wiki/
1.下载源码包和运行包
2.运行mysql.sql脚本 建立一个seata数据库 导入该sql 源码包:seata-1.3.0\script\server\db\mysql.sql 该脚本会创建了三 张表。这三张表都是用于保存整个系统中分布式事务相关日志数据的。
3.修改seata运行包seata-server-1.3.0\seata\conf目录下的file.conf文件 配置stata服务存放日志的位置 这里选择用mysql
4.修改seata运行包seata-server-1.3.0\seata\conf目录下的registry.conf文件 配置stata服务要连接的配置中心和注册中心
5.修改seata源码包下的seata-1.3.0\script\config-center目录下的config.txt文件复制一份到seata运行包下的根目录下
6.对其config.txt进行修改:修改其数据库的配置
7.运行seata源码包下的seata-1.3.0\script\config-center\nacos目录下有一份nacos-config.sh配置文件 将其复制到seata运行包下seata-server-1.3.0\seata\config.txt这个文件的下一级目录 也就是seata-server-1.3.0\seata\bin目录下 在该目录下执行 sh nacos-config.sh -h 127.0.0.1
8.启动seata服务
在seata运行包目录下seata-server-1.3.0\seata\bin有个seata-server.bat脚本 执行命令:seata-server.bat -m db -p 8091 -h 127.0.0.1 启动 为止 seata服务的配置和启动就搞定了 接下来定义两个工程来验证分布式事务
验证
需求:定义两个服务 1.purchase(采购服务) 2.stock(库存服务)
入口:采购服务定义一个接口 1.添加一条采购记录 2.调用库存服务 添加一条库存记录对于系统来说是两个事务 但对于我们的业务来说 就是一个完整的事务
环境构建purchase采购服务:
1.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-alibaba-sxw-parent</artifactId>
<groupId>com.sxw</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>07-seata-purchase</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
<!-- seata依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--nacos config 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos discovery依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--actuator依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!--修改MySQL驱动版本-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.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.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.配置文件boostrap.yml
spring:
application:
name: sxw-purchase-seata
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
group: SEATA_GROUP
seata:
tx-service-group: my_test_tx_group
logging.level.io.seata: debug
配置中心的配置:
server:
port: 8081
spring:
cloud:
nacos:
disvovery:
server-addr: 127.0.0.1:8848
jpa:
generate-ddl: true
show-sql: true
hibernate:
ddl-auto: none
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/kaikeba?
username: root
password: root
logging:
level:
root: info
org.hibernate: info
org.hibernate.type.descriptor.sql.BasicBinder: trace
org.hibernate.type.descriptor.sql.BasicExtractor: trace
com.abc.provider: debug
3.配置代理数据源 注意:使用seata需要接入seata的代理数据源 这里以jpa为例子 常用的还有 mybatis 需要注意区分开
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
* @param druidDataSource The DruidDataSource
* @return The default datasource
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
4.采购实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@JsonIgnoreProperties({"hibernateLazyInitializer","handler","fieldHandler"})
public class Purchase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;//采购资源名称
private Integer count;//采购量
}
5.定义采购接口
@RestController
@RequestMapping("vm")
public class PurchaseController {
@Autowired
private PurchaseService purchaseService;
@Autowired
private RestTemplate restTemplate;
@GlobalTransactional//TM 事务的管理器
@RequestMapping("/purchase/add/{deno}")
//采购服务控制器 入口
public boolean addPurchaseHandle(@RequestBody Purchase purchase
,@PathVariable("deno") int deno) {
//添加一条采购记录
purchaseService.addPurchase(purchase);
//添加一条库存记录 另外一个服务。。。
Stock stock = new Stock();
stock.setName(purchase.getName());
stock.setTotal(purchase.getCount());
String url = "http://sxw-stock-seata/stock/add";
Boolean result = restTemplate.postForObject(url, stock, Boolean.class);
if (result != null) {
return result;
}
return false;
}
}
6.添加undo.log表 在源码包seata-1.3.0\script\client\at\db的mysql.sql文件 启动服务
环境构建库存服务
1.pom.xml 是一样的
2.boostrap.yml
# 配置配置中心的服务地址
spring:
application:
# 配置应用名称
name: sxw-stock-seata
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yml
group: SEATA_GROUP
seata:
tx-service-group: my_test_tx_group
配置中心:
erver:
port: 8082
spring:
cloud:
nacos:
disvovery:
server-addr: 127.0.0.1:8848
jpa:
generate-ddl: true
show-sql: true
hibernate:
ddl-auto: none
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/kaikeba?
username: root
password: root
logging:
level:
root: info
org.hibernate: info
org.hibernate.type.descriptor.sql.BasicBinder: trace
org.hibernate.type.descriptor.sql.BasicExtractor: trace
com.abc.provider: debug
3.代理数据源 和采购服务是一样的 为什么?因为既然要使用分布式事务 需要各个事务参与者都要被seata服务检测到 作为一个seata-client的角色
4.库存实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@JsonIgnoreProperties({"hibernateLazyInitializer","handler","fieldHandler"})
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;//库存名称
private Integer total;//资源数量
}
5.定义库存接口
@RestController
public class StockController {
@Autowired
private StockService stockService;
@RequestMapping("/stock/add")
public boolean addStockHandle(@RequestBody Stock stock) {
//手动异常
throw new RuntimeException("bug is coming~");
// return stockService.addStock(stock);
}
}
6.添加undo.log表 在源码包seata-1.3.0\script\client\at\db的mysql.sql文件 启动服务
测试:采购服务执行正常插入一条采购数据,调用库存服务,库存服务异常,此时我们看下数据库的结果是几条数据?
当然是不变 这样就确保了分布式事务的一致性。
注意:seata使用的数据库的三个表会发现数据一直都是空的。为什么?因为seata默认使用的是AT模式,AT模式事务处理完毕会自动清楚日志,因此我们是看不到数据的。我们可以在一个分支事务提交后打一个断点,此时就可以看到存了一些数据,具体内容是一些sql拼装以及整个分布式事务的一些信息,如果所有分支事务都完成了,那么提交,否则根据拼装的sql进行回滚数据。