理论基础
CAP指的是一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可以采用适当的采取弱一致性,即:最终一致性。
BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。
BASE理论是柔性事物的理论基础。
分布式事务常用方案
2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如:互联网金融企业最核心的三个服务:交易、支付、账务。
Saga:由于Saga事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga由于缺少预提交动作,导致补偿动作的实现比较麻烦,例如:业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。所以,Saga事务较适用于补偿动作容易处理的场景。
最终一致性:适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
seata组成结构
seata组成结构:
- Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
- Transaction Manager (TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
- Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。
seata客户端启动流程
1)自动加载Bean属性和配置信息。
2)初始化TM。
3)初始化RM。
4)初始化分布式事务客户端完成,完成代理数据库配置。
5)连接TC(seata服务端),注册RM和TM。
6)开启全局事务。
seata-AT模式(官方推荐模式)
AT模式主要分为两个阶段:
第一阶段
执行流程:
- 解析SQL,获取SQL类型(CRUD)、表信息、条件(如:where) 等相关信息。
- 查询前镜像(改变之前的数据),根据解析得到的条件信息,生成查询语句,定位数据。
- 执行业务SQL,更新数据。
- 查询后镜像(改变后的数据),根据前镜像的结果,通过主键定位数据。
- 插入回滚日志,将前后镜像数据以及业务SQL等信息,组织成一条回滚日志记录,插入到Undo Log表中。
- 提交前,向TC注册分支,申请全局锁。
- 本地事务提交,业务数据的更新和生成的Undo Log一起提交。
- 将本地事务提交的结果通知给TC。
第二阶段
如果没问题,执行提交操作:
执行流程:
- 收到TC分支提交请求,将请求放入到一个异步任务的队列中,返回提交成功的结果给TC。
- 异步任务阶段的分支提交请求删除Undo Log中记录。
如果有问题,执行回滚操作:
执行流程:
- 开启本地事务,通过XID和BranchID查找到对应的Undo Log记录。
- 根据Undo Log中的前镜像和业务SQL的相关信息生成并执行回滚语句。
- 提交本地事务,将本地事务的执行结果(分支事务回滚的信息)通知给TC。
seata分布式事务执行示例
如图所示:
执行步骤:
1)TM(订单服务)向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID,XID在微服务调用链路的上下文中传播。
2)RM(订单服务)向TC注册分支事务,执行业务SQL和UNDO_LOG数据的插入。在提交本地事务前,RM会向TC申请关于相关记录的全局锁:
- 如果申请到了相关记录的全局锁,则直接提交本地事务,并向TC汇报本地事务执行成功(此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令)。
- 如果申请不到相关记录的全局锁,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。
3)执行远程服务调用,即:RM(库存服务)向TC注册分支事务,执行业务SQL和UNDO_LOG数据的插入。执行过程与订单服务一致。
4)TC根据所有的分支事务执行结果,向RM下发提交或回滚命令。
5)RM如果收到TC的提交命令,则释放相关记录的全局锁,再将提交请求放入一个异步任务的队列中,返回提交成功的结果给TC。异步队列中的提交请求执行时,只是删除UNDO LOG表中对应的记录。
6)RM如果收到TC的回滚命令,则会开启一个本地事务,通过XID和Branch ID查找到UNDO LOG表中对应的记录。将UNDO LOG中的后镜像数据与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况需要根据配置策略来做处理。否则,根据UNDO LOG中的前镜像数据和业务SQL的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,最后释放相关记录的全局锁。
UNDO_LOG表中的记录示例
{
"branchId": 641789253,
"undoItems": [
{
"afterImage": {
"rows": [
{
"fields": [
{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}
]
}
],
"tableName": "product"
},
"beforeImage": {
"rows": [
{
"fields": [
{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}
]
}
],
"tableName": "product"
},
"sqlType": "UPDATE"
}
],
"xid": "xid:xxx"
}
为什么seata在第一阶段就直接提交了分支事务?
seata能够在第一阶段直接提交事务,是因为seata为每一个RM维护了一张UNDO_LOG表(这张表需要客户端自行创建),其中保存了每一次本地事务的回滚数据。因此,二阶段的回滚并不依赖于本地数据库事务的回滚,而是RM直接读取UNDO_LOG表的记录,并将数据库中的数据更新为UNDO_LOG中存储的历史数据(前镜像数据和业务SQL)。这也是在使用seata作为分布式事务解决方案时,需要在参与分布式事务的每一个服务中加入UNDO_LOG表。
如果第二阶段是提交命令,则RM并不会对数据进行提交(因为第一阶段已经提交),而是发起一个异步请求删除UNDO_LOG中关于本事务的记录。
注意:由于seata在第一阶段直接提交了本地事务,会造成隔离性问题,因此Seata的默认隔离级别为:Read Uncommitted,而seata也支持Read Committed的隔离级别。
seata使用存在的问题
seata事务代理只是代理局部各自的事务,在原生SQL的前后记录了操作的信息,存储在undo_log表中,seata是采用undo_log生成逆向SQL回滚操作。seata的局部事务已经写到了库中,避免了死锁现象,但容易出现脏读的情况(因为seata默认隔离级别为Read Uncommitted)。
seata-Read Committed隔离级别
seata由于一阶段RM自动提交本地事务的原因,默认隔离级别为Read Uncommitted。如果希望隔离级别为Read Committed,那么可以使用SELECT…FOR UPDATE语句。seata引擎重写了SELECT…FOR UPDATE语句执行逻辑,SELECT…FOR UPDATE语句的执行会申请全局锁 ,如果全局锁被其他事务持有,则释放本地锁(回滚SELECT…FOR UPDATE语句的本地执行)并重试。这个过程中,查询会被阻塞,直到拿到全局锁。
seata实现2PC与传统2PC的差别
架构层次:传统2PC方案的RM实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而Seata的RM是以jar包的形式作为中间件层部署在应用程序侧。
两阶段提交:传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到第二阶段完成才释放。而Seata的做法是在第一阶段就将本地事务提交,这样就可以省去第二阶段持锁的时间,整体提高效率。
seata使用示例
分布式事务seata的使用请查阅本人发布的另外一篇seata使用示例文章。