大家好,最近看了一些seata的书,本着大家相互交流共同提升的原则,做了以下的笔记,所有笔记都是自己一点一点码字上去,会根据自己学习的进度持续更新,希望我们都能早日成为大牛,人不在江湖,江湖仍有你的传说
第一章:事务与分布式事务
1.1 事务与ACID特性
1.1.1 原子性
定义:一个事务必须被视为一个不可分割的最小工作单元,事务中的所有操作要么全部提交成功,要么全部失败回滚,不可能只执行其中的一部分操作
1.1.2 一致性
定义:数据库从一种有效状态变成另一种有效状态,写入数据库的数据必须满足所定义的规则,(比如字段的值不能小于0,更改这个值之前和更改这个值以后都是符合约束的,满足数据库层面的一致性或者业务上的正确状态)
1.1.3 隔离性
定义:一个事物所做的修改在最终提交之前,对其他事物时不可见的,严格来说,只有”可串行化“的隔离级别符合这个要求,但是性能会大大降低
mysql幻读
MySQL 可重复读隔离级别,完全解决幻读了吗? | 小林coding
1.1.3 持久性
定义:一旦一个事物被提交了,则数据库中数据的改变时永久的
1.2 XA两阶段提交协议
定义:作为资源管理器与事物协调器的接口标准,被用来管理分布式事务, 可以保证数据的强一致性
XA协议包括两套函数:以xa_开头的函数与ax_开头的函数
- xa_open()、xa_close():建立/关闭与资源管理器的连接
- xa_start()、xa_end():开始/结束一个本地事务
- xa_prepare()、xa_commit()、xa_rollback(): 预提交/提交/回滚一个本地事务
-
xa_remove(): 回滚一个已经预提交的事务
-
ax_reg()、ax_unreg(): 允许一个资源管理器在事务协调器中动态注册/撤销注册
1.2.1 两阶段提交协议的执行过程
第一阶段:
-
应用程序向事务协调器发起提交请求,事务协调器通知参与该事务的资 源管理器准备事务
-
第二阶段:资源管理器在接收到消息后开始准备(写好事务日志并执行事务,但不 提交)之后将就绪的消息提给事务协调者
- 事务协调器在收到各个资源管理器回复的消息后,基于投票进行决策— 提交还是会滚。如果任意一个回复失败,则发出回滚的命令
- 各个资源管理器在接收到二阶段提交或回滚命令后,执行并将结果返给 事务协调器
1.2.2 两阶段提交协议的缺点 - 同步阻塞问题,在执行过程中,所有参与节点都是事务阻塞型的
- 单点故障 ,一旦事务协调器发生故障,参与者会一直阻塞下去
- 数据不一致 ,如发送commit过程中事务协调器发生了故障,则会导致只有一部分参与者接收到了commit请求
- 状态不确定:如果事务协调器在发出commit请求以后宕机了,且唯一接收到这条消息的参与者也宕机了,即使通过选举产生了新的事务协调器,这条事务的状态也是不确定的,因为没人知道事务时都已经被提交过
1.3 CAP和BASE理论
1.3.1 CAP理论
一个分布式系统不可能同时满足一致性(C)、可用性(A)、和分区容错性(P)
- 一致性:all nodes see the same data at the same time
- 可用性:reads and writes always succeed
- 分区容错性:分布式系统遇到某节点或网络分区故障时,仍然能够对外提供满足一致性和可用性的服务
CP:要求一致性和分区容错性 这种模式性能偏低,常用的方案有XA两阶段提交、Seata AT模式的”读已提交级别“
AP:实现可用性和分区容错性,此组合为最终一致性方案,性能高,可以满足高并发的要求,常用的方案有TCC、基于消息的最终一致性、Saga等
1.3.2 BASE理论
- 基本可用(Basically AvailAble ):是对可用性的一个妥协,即在分布式系统中存在不可预知的故障时,允许损失部分可用性 比如秒杀业务的降级处理
- 软状态(Soft State):软状态是相对原子性而言的,允许系统在多个不同节点的数据副本上存在数据延时
- 最终一致性(Eventually Consistency):不能一直是软状态,要求有时间期限,在时间期限后,应当保证所有副本保持数据一致性
1.4 TCC 柔性事务
- 业务应用向事务协调器发起事务请求
- 业务应用调用所有服务的try接口,完成一阶段工作
- 业务应用根据调用try接口是否都成功,决定提交或回滚事务,并发送请求到事务协调器
- 事务协调器根据接收到的请求为提交还是会滚事务,决定调用confirm接口还是cancel接口,如果接口调用失败,则会重试,所以confirm接口和cancel接口必须幂等
1.5 基于消息的一致性解决方案
第二章:Seata 原理详解
2.2 Seata总体架构
2.2.2 逻辑结构
seata有三个主要角色: TM(事务管理者)、RM(资源管理器)、TC(事务协调者)其中TM和RM以SDK的形式作为Seata的客户端与业务系统集成到一起,TC作为Seata的服务端独立部署
主要流程
- TM开启全局事务(TM向TC开启全局事务)
- 事务参与者通过RM与资源交互,并注册分支事务(RM向TC注册分支事务)
- 事务参与者在完成了资源操作以后,上报分支事务的状态(RM向TC上报分支事 务状态)
- TM结束全局事务,事务一阶段结束(TM向TC提交/回滚全局事务)TODO
- TC推进事务二阶段操作(TC向RM发起二阶段提交/回滚)
第三章:Seata AT模式
3.1 AT模式的基本原理
在AT模式中,是通过Seata的数据源代理DataSourceProxy类对数据库进行操作,在业务通过JDBC标准接口访问数据库资源时,数据源代理会拦截所有的请求,除执行原始请求外,还会做一些与分布式事务相关的工作,包括产生前镜像、产生后镜像、加锁数据、保存事务日志等
- 在Seata中,每一个参与AT模式事务的数据库被看作一个资源,在每个本地事务提交前,Seata资源管理器(RM)都会向事务协调器(TC)注册一个分支事务,每一个分支事务都会对应于插入的一行事务日志。注册分支事务后,会提交本地事务,在本地事务提交后,资源管理器RM向事务协调器TC汇报分支事务状态
- 在全局事务中的所有操作都完成后,事务管理器(TM)根据“执行时是否捕获异常” 来决定提交全局事务还是会滚全局事务,通知事务协调器提交或回滚分布式事务,进入二阶段处理
- 事务协调器找出该分布式事务的所有分支事务,向每个分支事务所对应的资源管理器发起二阶段提交或二阶段回滚操作,资源管理器根据分支事务ID,从事务日志表找到对应的事务日志,并基于日志完成二阶段处理
3.1.1 工作流程示例
注:两个服务,余额服务管理账户的余额并调用“积分服务”,积分服务负责管 理用户的积分
AT服务主要有两个阶段,一阶段的流程图为:
二阶段:
TC收到全局事务的提交/回滚指令后,发起二阶段处理:
- 如果是全局事务提交,则TC通知多个RM异步的清理本地的事务日志
- 如果是全局事务回滚,则TC通知每个RM回滚数据
3.1.1 事务日志表
在AT模式中,每一个参与事务的业务数据库都会创建一张事务日志表
表信息:
- 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;
- 注:rollback_info是事务日志表的核心字段,记录了回滚的数据信息,其中包含beforeImage和afterImage
-
对于INSERT操作,前镜像是空的,后镜像是新插入行的数据,
-
对于DELETE操作,前镜像是删除之前的数据,后镜像是空的,
-
对于UPDATE操作,前镜像是更新前的数据,后镜像是更新后的数据
afterImage中的数据,包含一下3层:
-
io.seata.rm.datasource.sql.struct.TableRecords
-
io.seata.rm.datasource.sql.struct.Row
-
io.seata.rm.datasource.sql.struct.Field
源码解读
一. DataSourceProxy
public DataSourceProxy(DataSource targetDataSource) {
this(targetDataSource, DEFAULT_RESOURCE_GROUP_ID);
}
构造函数执行过程:
1.将传入的原始数据源作为目标数据源
2.初始化init()方法
2.1 首先建立一个连接,使用这个连接得到url地址、数据库类型、用户名称等信息,
2.2 把本数据源代理注册到ResourceManager中
2.3 如果开启了数据源检查,会定时刷新表的元数据信息缓存
resouceId:jdbcUrl
二. ResourceManager
- register(Resource resource)
首先是DefaultResourceManager的redister方法,
采用SPI机制加载实现类,加载后放在Map<BranchType,ResourceManager>中,对于AT模式来说,得到的对象是DataSourceManager
DataSourceManager
1. 注册资源先加入到本地缓存 Map<String, Resource>
2. 再调用父类的方法,通过资源管理器的RPC客户端(RmRpcClient类)将当前资源的资源组ID和资源ID发送给事务协调器,从而注册当前资源
3. 事务协调器收到资源注册的请求后,会把客户端连接与资源组ID及资源ID在内存中 建立关系,在推进二阶段提交或者回滚时,可以根据资源组ID或资源ID找到可用的客户端连接,并发送请求,这种机制保证了二阶段操作的高可用
三. ConnectionProxy
@Override
public void co
mmit() throws SQLException {
try {
lockRetryPolicy.execute(() -> {
doCommit();
return null;
});
} catch (SQLException e) {
if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) {
rollback();
}
throw e;
} catch (Exception e) {
throw new SQLException(e);
}
}
关键方法:
private void doCommit() throws SQLException {
if (context.inGlobalTransaction()) {
//分支事务提交
processGlobalTransactionCommit();
} else if (context.isGlobalLockRequire()) {
//本地事务提交并查询全局锁
processLocalCommitWithGlobalLocks();
} else {
//本地事务提交
targetConnectio
n.commit();
}
}
processGlobalTransactionCommit();
-
注册分支事务,返回branhcId
-
保存undo_log表
-
本地事务提交
-
如果发生了异常向TC报告该branchId分支事务状态,如果本地事务提交失败,这个分支事务所做的工作并没有入库,所以无需进行二阶段处理
processLocalCommitWithGlobalLocks();
该方法查询是否有全局锁,然后提交本地事务,为了防止分布式事务中的脏 读, 达到读已提交状态