在说分布式事务之前先说一下本地事务的实现
本地事务有四大特性,ACID,分别是原子性,一致性,隔离性,持久性:
原子性(Atomicity)
要么都执行,要么都不执行
一致性(Consistency)
事务前后的数据都是正确的
隔离性(Isolation)
事物之间相互隔离,互不干扰(并发执行的事务彼此无法看到对方的中间状态)
持久性(Durability)
事务一旦提交不可再回滚
以MySQL数据库为例,其实现本地事务的秘诀主要靠的是undo日志和redo日志
其执行流程大致如下:
a.本地事务开启 -->
b.undo buffer 记录事务执行前状态(在内存中)-->
c.事务执行 -->
d.redo buffer 记录事务执行后状态(在内存中)-->
e.undo buffer 日志写入 redo日志-->
f.redo日志写入硬盘(顺序写入)
g.提交事务 -->
如果事务执行过程中发生异常,如在d阶段,则MySQL会根据undo日志回滚至事务执行前的状态,如果MySQL这时候宕机了,则恢复后会通过redo日志进行数据校验对比。有两种模式,一是扫描redo日志中所有undo日志的部分,然后对数据库的数据进行校验,将数据恢复至事务前的状态,另一种是补偿策略,先将redo日志中的内容全部执行,然后再通过undo日志进行补偿恢复。并且数据库的持久化是异步执行的,因为数据已经通过redo日志持久化到了硬盘里,而且redo日志是顺序写入,效率高(数据库由于表结构及索引的问题是随机写入)。
通过日志机制保证了事务的原子性与持久性,隔离性则是通过锁来实现,数据库的锁机制放在下篇文章讲。
1、典型的场景就是微服务架构,微服务之间通过远程调用完成事务操作。 比如:订单微服务和商品微服务,下单的同时订单微服务请求商品微服务减库存。简言之:跨JVM进程产生分布式事务。
2、单体系统访问多个数据库实例 当单体系统需要访问多个数据库(实例)时就会产生分布式事务。 比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。 简言之:跨数据库实例产生分布式事务。
3、多服务访问同一个数据库实例 比如:订单微服务和商品微服务即使访问同一个数据库也会产生分布式事务,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。简言之:跨JVM进程产生分布式事务。
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性
(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同
时实现两点,不可能三者兼顾。
它们的定义如下:
一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。
可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容错性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数
据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
先解释下分区容错性,分布式系统搭建集群,通过子网路通信,现在一个服务器在北京,一个服务器在上海,则这两个服务器之间通信很有可能由于网络的原因导致通信超时或失败。这是传输链路的问题,除非是单体架构,否则一定会出现分区容错性的问题,既然分区容错一定会出现,根据ACPP原则,一致性跟可用性就不可能同时保证,处理分布式事务一般也是在这二者中做取舍
一致性通俗来说,就是一个客户端像北京的服务器发送了一条数据,北京的服务器给他返回的值肯定是他发送的那条,但负载均衡不一定会让响应他的服务器是北京那台,如果上海的服务器给他返回的信息不是他发送的,那就不满足一致性
可用性比较好理解,我发送一个请求,不管你处理结果如何,都得给我一个响应(对错均可),保证服务可用
综合这几点来看,一致性与可用性的冲突源自分区容错性,要保证强一致,服务器需要时间去进行数据同步,并且在处理写操作的时候锁定其他服务的读写操作,这个过程就不可用。
可用性同理,保证高可用,势必不能锁定。
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
在处理分布式事务,我们一般保证基本可用与最终一致性,将处理数据一致性的时间降低至可接受的范围内,保证了数据最后还是一致的,这个过程中服务也不是完全可用,叫基本可用。
分布式事务处理模型应当包括以下几个角色:
应用程序(APP): 微服务
事务管理器(TM):全局事务管理者
资源管理器(RM):一般是数据库
通信资源管理器(CRM):TM与RM通信的中间件
两阶段提交是处理分布式事务最成熟的方式,其核心思想在于将分布式事务拆分成一个个的本地事务,为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。在一阶段的时候,协调者询问各个参与者是否可以将事务提交,二阶段才讲事务真正提交给事务数据源。
阶段一
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。阶段二
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,
发送提交(commit)消息。两种情况处理如下:情况1:当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
两阶段提交存在的问题:
1.性能问题:所有参与者在事务提交阶段处于同步阻塞状态,暂用了很大的系统资源,容易导致性能瓶颈
2.可靠性问题(数据库死锁问题):如果协调者存在单点故障,则所有的参与者将会一直占用锁,不能释放,其他事务无法访问
3.数据一致性问题:如果协调者与参与者同时宕机,有可能导致数据不一致。
总结一下两阶段提交,尽量保证了数据的强一致性,适合对数据强一致要求很高的关键领域。(由于问题3的存在,其实也不能百分百保证强一致,但这种情况出现的概率很小)。牺牲了可用性,对性能要求大,不适合高并发使用。
三阶段提交是在二阶段提交上的改进版本,3PC最关键要解决的就是协调者和参与者同时挂掉的问
题,所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交。
阶段一
a) 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
b) 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
阶段二
协调者根据参与者响应情况,有以下两种可能。
情况1:所有参与者均反馈 yes,协调者预执行事务
a) 协调者向所有参与者发出 preCommit 请求,进入准备阶段。
b) 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
情况2:只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断事务
a) 协调者向所有参与者发出 abort 请求。
b) 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
阶段三
该阶段进行真正的事务提交,也可以分为以下两种情况。
情况 1:所有参与者均反馈 ack 响应,执行真正的事务提交
a) 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。
b) 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:只要有一个参与者反馈 no,或者等待超时后协调组尚无法收到所有提供者的反馈,即回滚事务。
a) 如果协调者处于工作状态,向所有参与者发出 rollback 请求。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调组反馈 ack 完成的消息。
d) 协调组收到所有参与者反馈的 ack 消息后,即完成事务回滚
与二阶段提交相比,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务,避免了协调者单点问题,在阶段三中,如果协调者出现问题,参与者会继续提交事务。但是数据不一致的问题依旧存在,如果参与者收到preCommit请求后,等待do commit指令时,协调者请求中断事务,而此时出现故障,则参与者会继续提交事务,导致数据不一致。
TCC补偿事务是服务化的两阶段提交编程模型,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
它分为三个步骤(两个阶段,Confirm与Cancel是一个阶段):
Try 阶段主要是对业务系统做检测及资源预留。
Confirm 阶段主要是对业务系统做确认提交。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放
在执行的时候,Try阶段对业务系统做检测及资源预留,比如A给B转账50元,它的账户有100元(需要在数据库加一个冻结的字段),在转账时会将50插入到冻结字段,余额还是100。这个阶段的事务是直接提交的,所以不会造成死锁与阻塞问题。
如果Try阶段执行成功了,Confirm则将冻结字段的金额解冻,并且从余额中扣除金额,提交事务。
如果Confirm阶段执行出现异常,则Cancel阶段进行回滚,通过反向操作(Try阶段在冻结字段添加了50,Cancel减少50)
TCC模式使具体业务实现控制锁的粒度变小,不会锁定整个锁资源。并且基于Confrim和Cancel的幂等操作,保证了数据的一致性,同时解决了单点故障问题,由业务发起方控制整个业务活动,业务活动管理器也变成了多点,引入集群。但是对应的,业务变现变得复杂,原本的一个业务操作变成了三个业务操作,侵入性强,耦合度高,开发的成本高
通过MQ中间件来实现,可以看我这篇文章:消息队列之RabbitMQ的五种消息模型,及如何保证可靠消息最终一致性_Alex-HH的博客-CSDN博客
Seata提供了四种不同的分布式事务解决方案:
XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入.
TCC模式:最终一致的分阶段事务模式,有业务侵入
AT(auto transaction)模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式.
SAGA模式:长事务模式,有业务侵入
一般我们使用Seata的AT模式
Seata事物管理中有三个重要的核心组件:
TC(Transaction Coordinator)-事务协调者:维护分支事务的状态,协调全局事务提交或回滚。
TM(Transaction Manager)-事务管理器:定义全局事务的范围,并开始全局事务、提交或回滚全局事务。
RM(Resource Manager)-资源管理器:向TC注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行并上报,如下图所示:
下面我们通过一个分支事务的执行过程来了解 Seata 的工作流程。
例如有一个业务表 product(id,name),分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
一阶段:
1、解析SQL:
得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。
2、查询前镜像
根据解析得到的条件信息,生成查询语句,定位数据。
select id, name from product where name = 'TXC';
执行查询语句,得到前镜像
id 1
name TXC
3、执行业务SQL
update product set name = 'GTS' where name = 'TXC';
把
name
改为了GTS
。
4、查询后镜像
根据前镜像的结果,通过 主键 定位数据。
select id, name from product where id = 1;
执行查询语句,得到后镜像
id 1
name CTS
5、插入回滚日志
把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG
表中。
6、注册分支事务
RM向TC注册分支事务将其其纳入XID对应全局事务的管辖,并申请 product 表中主键值等于 1 的记录的全局锁 。
7、本地事务提交
业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
8、上报事务状态:
将本地事务提交的结果上报给 TC。
二阶段-回滚:
(1)收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
(2)通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
(3)数据校验
拿 UNDO LOG 中的后镜与当前数据进行比较,根据校验结果决定是否做回滚。
(4)根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
(5)提交本地事务
并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段-提交:
第二阶段全局事务提交,TC会通知各各分支参与者提交分支事务,在第一阶段就已经提交了分支事务,这里各各参与者只需要删除undo_log即可,并且可以异步执行,第二阶段很快可以完成。