1.分布式事务介绍
1.1.什么是分布式事务
····针对于单数据库的事务我们叫着本地事务/传统事务,在分布式环境中一个请求可能涉及到多个数据库的写操作(多数据源),要保证多数据源的一致性必须用到分布式事务。
1.2.为什么需要分布式事务
····系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。
····一个下单请求同时设计到订单库,优惠券库,库存库的写操作,需要保证三个库写操作的一致性,就要用到分布式事务 即:分布式事务就是要解决一个请求同时对多个数据库写操作的一致性。
····注意:微服务拆分原则,尽量让大部分操作都不要跨微服务操作,也就是跨库。 分布式事务比本地事务耗费的资源更多。
1.3.解决方案
····不同的分布式场景业界常见的解决方案有2PC、3PC、TCC、 可靠消息最终一致性、最大努力通知这几种。
2PC方案
····2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段( Prepare phase).提交阶段( commit phase ) , 2是指两个阶段, P是指准备阶段, C是指提交阶段。
····在第一阶段(准备阶段),事务管理器先事务参与者(资源)们发送准备请求,大家都返回OK状态,那么就进入第二阶段,提交事务,如果在第一阶段有任何一个参与者没有OK,那么事务协调器通知其他所有事务参与者(资源)回滚事务。2PC常见的标准是XA, JTA,Seata等。
二阶段能保证分布式事务的原子性,但是也有一些明显的缺陷。比如:
- 在第一阶段,如果参与者迟迟不回复协调者,就会造成事务的阻塞,性能不好。
- 单节点故障,如果协调器挂了,参与者会阻塞,比如在第二阶段,如果事务协调器宕机,参与者没办法回复信息,长时间处于事务资源锁定,造成阻塞(事务操作是要加锁的)。
- 在第二阶段,如果在事务协调器发出“commit”执行后宕机,一部和参与者收到了消息提交了事务,而一部分没有消息没法做出事务提交操作,这样就出现了数据不一致。
- 在第二阶段,如果事务事务协调器发出“commit”指令后宕机,收到“commmit”指令的参与者也宕机了,那么事务最终变成了什么效果,提交了还是没提交?没有谁知道。
三阶段提交见: https://blog.csdn.net/u014494148/article/details/105495687
基于XA的2pc
····2PC的传统方案是在数据库层面实现
的,如Oracle、MySQL都支持2PC协议,为了统一标准,减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织Open Group定义了分布式事务处理模型DTP
( Distributed Transaction Processing Reference Model )。
DTP模型定义如下角色
- AP(Application Program) :即应用程序,可以理解为使用DTP分布式事务的程序。
- TM(Transaction Manager) :事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生 命周期,并协调各个RM。
- RM(Resource Manager) :即资源管理器,可以理解为事务的参与者, 一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
DTP模型定义TM和RM之间通讯的接口规范叫XA ,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案。
以上三个角色之间的交互方式如下:
- TM向AP提供应用程序编程接口, AP通过TM提交及回滚事务。
- TM交易中间件通过XA接口来通知RM数据库事务的开始、结束以及提交、回滚等。
基于XA模式的2PC方法的问题 是:资源锁定时间长,性能查,它的资源释放需要等到二阶段提交完成后才会释放。
基于Seata的2pc
Seata是由阿里中间件团队
发起的开源项目Fescar ,后更名为Seata ,它是一个是开源的分布式事务框架
。传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务
,是工作在应用层的中间件
。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式
解决微服务场景下面临的分布式事务问题,它目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案。
- Transaction Coordinator(TC):事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。 相当于是一个软件需要单独部署
- Transaction Manager ™:事务管理器, TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终 向TC发起全局提交或全局回滚的指令。
- Resource Manager (RM):资源管理器控制分支事务, 负责分支注册、状态汇报,并接收事务协调器TC的指令, 驱动 分支(本地)事务的提交和回滚。
- 具体的执行流程如下:
- 用户服务的TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
- 用户服务的RM向TC注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入XID对应全局事务的管辖。
- 用户服务执行分支事务,向用户表插入一条记录。
- 逻辑执行到远程调用积分服务时(XID在微服务调用链路的,上下文中传播)。积分服务的RM向TC注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入XID对应全局事务的管辖。
- 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
Seata与XA的差别
····架构层次方面,传统2PC方案的RM实际上是在数据库层, RM本质上就是数据库自身,通过XA协议实现,而Seata的RM是以jar包的形式作为中间件层部署在应用程序这一侧的。
····两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback ,事务性资源的锁都要保持到Phase2完成才释放。而Seata的做法是在Phase1就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。
TCC方案
····TCC, 是基于补偿型
事务的AP
系统的一种实现, 具有弱一致性
补偿理解:按照事先预定的方案去执行,如果失败了就走补偿方案(撤销)。
常见的TCC方案
目前市面上的TCC框架众多比如下面这几种:
框架名称 | Github地址 | sart数量 |
---|---|---|
tcc-transaction | https://github.com/changmingxie/tcc-trancation | 3850 |
Hmily | https://hithub.com/yu199195/hmiliy | 2407 |
bytetcc | https://github.com/liuyangming/ByteTCC | 1947 |
EasyTransaction | https://github.com/QNJR-GROUP/EasyTranscation | 1690 |
TCC优缺点
- 优点 : 异步执行效率高
TCC能够对分布式事务中的各个资源进行分别锁定, 分别提交与释放, 例如, 假设有AB两个操作, 假设A操作耗时短, 那么A就能较快的完成自身的try-confirm-cancel流程, 释放资源. 无需等待B操作. 如果事后出现问题, 追加执行补偿性事务即可.
TCC是绑定在各个子业务上的(除了cancle中的全局回滚操作), 也就是各服务之间可以在一定程度上”异步并行”执行.
- 缺点
对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等-一个操作多次循环都是一样。
2.可靠消息最终一致性
····可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
最终一致性要解决三个问题:本地事务和消息发送的原子性,接收消息的可靠性,消息重复消费问题(幂等)
- 本地事务和消息发送原子性
本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。
- 事务参与方接收消息可靠性 ack机制
事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息
- 消息重复消息问题 幂等性校验
由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。要解决消息重复消费的问题就要实现事务参与方的方法幂等性。
3.最大努力通知
····最大努力通知服务表示在不影响主业务的情况下,尽可能地确保数据的一致性。它需要开发人员根据业务来指定通知规则,在满足通知规则的前提下,尽可能的确保数据的一致,以达到最大努力的目的。
目标:发起通知方(接口提供方)通过一定的机制最大努力将业务处理结果通知到接收方(接口调用方)。
具体包括:
- 有一定的消息重复通知机制。
因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。 10s 1min 10min 1h 5h 1d
- 消息校对机制。
如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知 方查询消息信息来满足需求。
- 消息处理方需要保证幂等性
场景:支付宝结果通知
区别
- 可靠消息最终一致性
系统A本地事务执行成功,通知系统B处理任务,通常通过MQ实现。一般适用于平台内部,对一致性要求相对较高(微服务的2个子系统之间)。
- 最大努力通知
所谓最大努力通知就是系统A用最大努力通知系统B,能不能成功,不做完全保证,如果没通知到位,系统B可以主动来调用系统A的接口查询结果状态。一般适用于跨平台业务,或对接了上方平台的业务场景(支付结果通知)。
=========================================================
····2PC最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待TM的决定,此时服务会阻塞并锁定资源。由于其阻塞机制和最差时间复杂度高,因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以及子事务生命周期较长(long-running transactions)的分布式服务中。
····但是2pc是强一致性的,如果业务对两个数据库数据的一致性要求较高,需要选择2PC方案,Stata是一个不错的选择,它屏蔽了传统2PC的诟病。
=========================================================
····如果拿TCC事务的处理流程与2PC两阶段提交做比较, 2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、 cancel三个操作。 此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。典型的使用场景:登录送优惠券等。
=========================================================
····可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注册送积分,登录送优惠券等。
=========================================================
····最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务 ;允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用 场景:银行通知、支付结果通知等。 如果有夸平台的业务接入,可以选择该方案。
4.Seata的使用
-
Transcation ID(XID) : 由事务协调者创建的全局唯一的事务ID
-
Transaction Coordinator(TC) - 事务协调器:一个独立运行的组件,负责维护全局事务的运行状态,负责根据TM的指令协调并驱动全局事务的提交或回滚,负责向资源管理器发起事务提交,回滚指令。
-
Transaction Manager - 事务管理器 :控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议,通知TC提交或者回滚事务。
-
Resource Manager(RM) - 资源管理器: 控制分支事务,负责分支事务注册、负责向TC汇报分支事务状态,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
1.安装SeataServer
下载:https://github.com/seata/seata/tags;
启动:seata-server.bat -p 8091 -h 127.0.0.1 -m file
配置文件可以不用修改
依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
yaml配置
seata:
enableAutoDataSourceProxy: false #关闭DataSource代理的自动配置,我们要手动配置
spring:
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group #这里和file.conf中事务组名一样
模块下创建配置文件
resources/file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroupMapping.fsp_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
resources/registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
password = ""
cluster = "default"
timeout = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
2.配置
启动类排除原来的DataSource
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
如果是MybatisPlus;
把DataSource交给Seata代理。
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 数据源代理
*/
@Configuration
public class DataSourceConfiguration {
//mapper.xml路径
@Value("${mybatis-plus.mapper-locations}")
private String mapperLocations;
//手动配置bean
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
//处理MybatisPlus
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSourceProxy);
factory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
//事务管理工厂
factory.setTransactionFactory(new SpringManagedTransactionFactory());
return factory;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
如果是Mybatis
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
//使用seata对DataSource进行代理
@Configuration
public class DataSourceProxyConfig {
//mapper.xml路径
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
//手动配置bean
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public SqlSessionFactory sessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSourceProxy);
sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
//事务管理工厂
sessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sessionFactoryBean.getObject();
}
@Bean
public DataSourceProxy dataSource() {
return new DataSourceProxy(druidDataSource());
}
}
2.3.开启Seata事务
在入口方法上:@GlobalTransactional(rollbackFor = Exception.class) 开启Seata全局事务;
将原来的@EnableTransactionManagement
去掉
2.4.创建undolog表
-- 注意此处0.3.0+ 增加唯一索引 ux_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.5.实物参与者
在其他模块重新按上面步骤操作,全局事务只需要在入口方法添加注解即可。