背景
本地事务在分布式系统中,只能控制住自己回滚,控制不了其他服务的回滚,在分布式系统中,事务如何保证,主要是有以下两个场景:
1.远程服务假失败问题,即远程服务业务其实成功了,但是由于网络故障,服务器环境等其他问题导致,远程调用异常(导致调用方业务回滚了,远程服务没有回滚)。
2.远程服务执行完成,下面的其他方法出现异常,导致已执行的的远程请求不能回滚。(无法控制远程请求的回滚)
本地事务
一、事务的基本性质
事务的几个特性:原子性,一致性、隔离性、持久性,简称ACID
-
原子性:事务的一系列操作不可拆分,要么全部完成,要么全部失败。
-
一致性:事务在执行前后,业务整体一致。比如转账,
A:100,B:100 转:A->B:50 结果:A:50,B:150 (结果是一致的,不可能存在A扣钱了,B钱没加上的情况)
-
隔离性:事务之前相互隔离(ABCD四个事务相互隔离执行,不会存在互相影响的情况)
-
持久性:一但事务成功,数据一定会落在数据库。
二、事务的隔离级别
- 读未提交(Read uncommitted)
该隔离级别的事务,会读到其他未提交事务的数据,此现象叫脏读。
- 读已提交(Read committed)
可以读到另一个事务已提交的数据,但是事务里面多次读取可能会出现结果不一致的情况(B事务在两次读取的中间提交了事务,修改了数据,造成前后数据不一样),此现象叫不可重复读。
- 可重复读(Repeatable read)
Mysql默认隔离级别,同一个事务里,select数据库读到的数据是事务开始状态时的数据,因此在这个事务里同样的select操作结果永远一致的,解决了不可重复读问题,但是会有幻读问题。
- 串行化(Serializable )
在该隔离级别下,所有事务都是串行执行的(类似于加锁)。避免了脏读、幻读、不可重复读,但是效率极低。
三、事务传播行为
指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。共7类传播行为,这里只讲最常用的:REQUIRED、REQUIRES_NEW。
REQUIRED:如果当前没有事务就创建一个事务,如果当前有事务,就加入该事务。(能上车就上车,没车上就自己来)
REQUIRES_NEW:创建一个新事务,不管当前有没有事务。(我不要你的,我要我自己的)
@Transactional //a事务
public void a() {
b(); // a事务(加入)
c(); // c事务(新事务,不回滚)
int i = 10/0; // 异常了c方法不回滚
}
@Transactional(propagation = Propagation.REQUIRED) // 有事务就加入,没有就创建
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // 创建新事务
public void c() {
}
四、事务失效问题
事务是用代理对象来控制的,同一类下调用事务方法,不会走到代理对象,就会导致事务失效。比如上面的a方法调b、c事务方法(同一类下)b、c方法事务就会失效。解决方法的核心就是想办法使用代理对象来调用:
1.使用AopContext获取当前代理类,使用代理对象来调用事务方法。需要在启动类加上@EnableAspectJAutoProxy(exposeProxy = true)
OrderServiceImpl orderServiceImpl = (OrderServiceImpl)AopContext.currentProxy();
orderServiceImpl.b();
2.通过spring上下文获取到当前代理类
/**
* 通过class获取Bean
* @return bean
*/
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
分布式事务
一、CAP理论 & BASE理论
在分布式系统中,一致性、可用性、分区容错性3个元素最多只能同时满足两个,一般来说分区容错性不可避免,因此可认为P总是成立,所以分布式事务都是使用的CP和AP方案。对于大多数场景下,主机众多,集群规模越来越大,所以节点故障,网络故障是常态,基本上是要保证A舍弃C的,即使用AP方案。
那我们一致性就不保证了吗?也并不是如此,AP方案下无法像本地事务一样保证强一致性,但是我们可以想办法做到弱一致性,即最终一致性,这就是BASE理论。其内容如下:
-
基本可用(Basically Available):指分布式系统出现故障的时候,允许损失部分可用性(如响应时间、错误引导页面等等),来保证核心业务可用。
-
软状态(Soft State):指系统允许在一个中间状态,并不是要么成要么败需要有一个同步中的状态。
-
最终一致性(Eventual Consistency):指系统中所有数据副本在经过一段时间后,最终能达到一致的状态。
二、分布式事务的几种方案
1. 2PC方案(也叫XA协议)
由数据库支持的一个叫事务管理器的东西,该协议分为两个阶段:
第一阶段:事务管理器要求每个涉及到的事务,做预提交操作,反映是否可以提交事务。
第二阶段:每个事务分别提交数据,其中,如果有任何一个事务否决提交,那么所有事务都会回滚这一次的操作。
2. 柔性事务—TCC事务(Try - Confirm - Cancel)
Try是指预留操作、Confirm是指确认操作、Cancel 是指回滚操作。因此,使用时我们每一个业务场景,都要设计实现这三步操作(要自己写业务回滚逻辑),缺点就是对业务的侵入比较大,紧耦合了。
3. 使用消息队列—事务消息
首先本地向消息中心发送事务消息(这个消息对于消费者来说暂时不可见),发送成功后执行本地事务。
再根据本地事务的结果向消息中心发送Commit或RollBck命令,如果是commit订阅方此时能接收到消息,并消费,如果是RollBck订阅方收不到这条消息,等于没执行过。
-
最大努力通知方案:不保证数据一定能够通知成功,但提供可查询操作接口进行核对。
-
可靠消息通知方案:可靠消息+最终一致性方案。
三、使用Seata控制分布式事务
Seata 是一款基于2PC方案的开源分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。流程如图:
![img](https://seata.io/img/solution.png)
TC - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata 提供了 AT、TCC、SAGA 和 XA 事务模式,这里以AT模式为例,演示一下如何使用:
1. 每一个微服务必须先创建undo_log表(回滚日志表)
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2. 安装事务协调器(Seata服务器),下载后将其解压。https://github.com/seata/seata/releases
3. SpringBoot整合seata:
1)导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2)解压并启动seata-server,找到目录下的seata\bin\seata-server.bat,启动。
需要注意修改两个配置文件,registry.conf:注册中心配置(我这里以nacos为注册中心),修改注册中心地址:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# type修改为nacos
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
...
config {
# file、nacos 、apollo、zk、consul、etcd3
# 指定配置类型,默认为文件,可以修改
type = "file"
...
file.conf文件配置,可选择按文件存储或db存储,db存储需要配置上数据库地址,我这里就选择以文件:
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "file"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "mysql"
password = "mysql"
}
}
3)所有想要用到的分布式事务的微服务使用seata的DataSourceProxy代理自己的数据源。每个服务添加如下配置类:
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
* @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());
}
return new DataSourceProxy(dataSource);
}
}
4)同时每一个微服务都必须配置上:registry.conf、file.conf,将这两个文件放在resources目录下,vgroup_mapping配置必须和应用名spring.application.name一致:
vgroup_mapping.{application.name}-fescar-server-group = "default"
5)至此,我们所有seata的配置已经全部完成了,接下来进行使用:
只需给我们的全局事务入口加上@GlobalTransactional(rollbackFor = Exception.class)
@GlobalTransactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
// 全局事务里面调用远程方法
wmsFeignService.orderLockStock(lockVo);
// 模拟代码异常
int i = 10/0;
}
接下来远程事务,我们不需要加@GlobalTransactional了,只需加上普通的事务注解@Transactional
@Transactional(rollbackFor = Exception.class)
public boolean orderLockStock(WareSkuLockVo vo) {
...
}
执行结果:在全局事务里面,调用完远程方法以后,程序抛了异常,结果远程事务也成功回滚了。
总结
Seata相当于是对2PC方案的一个升级,使用简单、上手快,SpringBoot也做了很好的支持。相较于普通用数据库实现二阶提交,性能上有一定优化。相较于传统TCC方案,对业务的侵入比较少,不用手动去写回滚代码。
但是!也是所有2PC方案的通病,无法支持高并发,一但流量上去了,整个系统的性能会大打折扣!
Seata只适合支持类似保存商品、提交资料等理论上不会出现大量请求,但是业务比较庞大复杂的场景。像那种下订单、抢票、秒杀等高并发场景还是要使用上我们的消息事务。在另外的文章里,我将会做详细的说明。
感谢阅读,再见!