本地事务&分布式事务

简介

分布式事务的目的是保障分布式存储中数据一致性,而跨库事务会遇到各种不可控制的问题,如个别节点宕机,像单机事务一样的ACID是无法奢望的

事务的四大特性 ACID

说到事务,就不得不提一下事务著名的四大特性。

  • 原子性
    原子性要求,事务是一个不可分割的执行单元,事务中的所有操作要么全都执行,要么全都不执行。
  • 一致性
    一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。
  • 隔离性
    事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。
  • 持久性
    持久性要求,一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

注意:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。

事务的隔离级别

  • Read uncommitted 读未提交
    在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。
    因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。

  • Read committed 读提交
    在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。

  • Repeatable read 重复读(数据库默认隔离级别)
    在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。

  • Serializable 序列化
    该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。

CAP理论

CAP理论说的是:在一个分布式系统中,最多只能满足C、A、P中的两个需求。

CAP的含义:

  • C:Consistency 一致性
    同一数据的多个副本是否实时相同。
  • A:Availability 可用性
    可用性:一定时间内 & 系统返回一个明确的结果 则称为该系统可用。
  • P:Partition tolerance 分区容错性
    将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。

CAP理论告诉我们,在分布式系统中,C、A、P三个条件中我们最多只能选择两个。那么问题来了,究竟选择哪两个条件较为合适呢?

对于一个业务系统来说,可用性和分区容错性是必须要满足的两个条件,并且这两者是相辅相成的。业务系统之所以使用分布式系统,主要原因有两个:

  • 提升整体性能
    当业务量猛增,单个服务器已经无法满足我们的业务需求的时候,就需要使用分布式系统,使用多个节点提供相同的功能,从而整体上提升系统的性能,这就是使用分布式系统的第一个原因。

  • 实现分区容错性
    单一节点 或 多个节点处于相同的网络环境下,那么会存在一定的风险,万一该机房断电、该地区发生自然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不同的地域、不同的机房中,从而保证系统高可用性。

BASE理论

CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性。下面来介绍下BASE理论。

  • BA:Basic Available 基本可用
    (1)整个系统在某些不可抗力的情况下,仍然能够保证“可用性”,即一定时间内仍然能够返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:
    (2)“一定时间”可以适当延长
    当举行大促时,响应时间可以适当延长
    (3)给部分用户返回一个降级页面
    给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。
  • S:Soft State:柔性状态
    同一数据的不同副本的状态,可以不需要实时一致。
  • E:Eventual Consisstency:最终一致性
    同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

本地事务注意事项

  1. 不要在接口上声明@Transactional ,而要在具体类的方法上使用 @Transactional 注解,否则注解可能无效。
  2. 不要图省事,将@Transactional放置在类级的声明中,放在类声明,会使得所有方法都有事务。故@Transactional应该放在方法级别,不需要使用事务的方法,就不要放置事务,比如查询方法。否则对性能是有影响的。
  3. 使用了@Transactional的方法,对同一个类里面的方法调用, @Transactional无效。比如有一个类Test,它的一个方法A,A再调用Test本类的方法B(不管B是否public还是private),但A没有声明注解事务,而B有。则外部调用A之后,B的事务是不会起作用的。(经常在这里出错)
  4. 使用了@Transactional的方法,只能是public,@Transactional注解的方法都是被外部其他类调用才有效,故只能是public。道理和上面的有关联。故在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,但事务无效。
  5. 经过在ICORE-CLAIM中测试,效果如下:
  • 抛出受查异常XXXException,事务会回滚。
  • 抛出运行时异常NullPointerException,事务会回滚。
  • Quartz中,execute直接调用加了@Transactional方法,可以回滚;间接调用,不会回滚。(即上文3点提到的)
  • 异步任务中,execute直接调用加了@Transactional方法,可以回滚;间接调用,不会回滚。(即上文3点提到的)
  • 在action中加上@Transactional,不会回滚。切记不要在action中加上事务。
  • 在service中加上@Transactional,如果是action直接调该方法,会回滚,如果是间接调,不会回滚。(即上文3提到的)
  • 在service中的private加上@Transactional,事务不会回滚。

分布式事务解决场景

1、跨数据库分布式事务
2、微服务分布式事务

分布式事务

本地事务&分布式事务_第1张图片

两阶段提交2pc(XA Transactions)

在分布式系统中,每个节点虽然可以知晓自己的操作是成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交。
本地事务&分布式事务_第2张图片
两阶段提交协议中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
多个事务参与者(participants):即本地事务执行者
工作流程

(1)投票阶段(voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。
参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
(2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交
还是回滚;
如果所示 1-2为第一阶段,2-3为第二阶段
如果任一资源管理器在第一阶段返回准备失败,那么事务管理器会要求所有资源管理器在第二阶段执行回滚操作。
通过事务管理器的两阶段协调,最终所有资源管理器要么全部提交,要么全部回滚,最终状态都是一致的

本地事务&分布式事务_第3张图片
缺点:

1)同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不
得不处于阻塞状态

2)单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所
有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是
无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

3)数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit
请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会
执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性
的现象

4)二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。
那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交

5)XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。TCC是业务层面的分布式事务,
最终一致性,不会一直持有资源的锁。

三阶段提交(3PC)

本地事务&分布式事务_第4张图片

  1. 相比2PC改变:
1)引入超时机制。同时在协调者和参与者中都引入超时机制
2)在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,
除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶
段。
  1. 流程:
1)CanCommit阶段
	3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,
	否则返回No响应
	(1)事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应
	(2)响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进
	入预备状态。否则反馈No
2)PreCommit阶段	
	协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。假如协调者
	从所有的参与者获得的反馈都是Yes响应,那么就会进行事务的预执行
	(1)发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段
	(2)事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中
	(3)响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令
	假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的
	中断
	(1)发送中断请求 协调者向所有参与者发送abort请求
	(2)中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断
3)doCommit阶段(该阶段进行真正的事务提交,也可以分为以下两种情况)			
	执行提交
	(1)发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送
	doCommit请求
	(2)事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源
	(3)响应反馈 事务提交完之后,向协调者发送Ack响应
	(3)完成事务 协调者接收到所有参与者的ack响应之后,完成事务
	中断事务(协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行
	中断事务)
	(1)发送中断请求 协调者向所有参与者发送abort请求
	(2)事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后
	释放所有的事务资源
	(3)反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
	(4)中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断
  1. 解决问题:
1)单点故障问题
2)减少阻塞	
一旦参与者无法及时收到来自协调者的信息之后,会默认执行commit,而不会一直持有事务资源并处于阻塞状态
  1. 缺陷:
1)一致性问题:由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了
commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况	

Try Confirm Cancel(TCC)(TCC框架github地址)

本地事务&分布式事务_第5张图片
1.概念
TCC 将事务提交分为 Try - Confirm - Cancel 3个操作。其和两阶段提交有点类似,Try为第一阶段,Confirm - Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。

1)Try:完成所有业务检查,预留必须业务资源

2)Confirm:真正执行业务,不作任何业务检查;只使用Try阶段预留的业务资源;Confirm操作满足幂等性

3)Cancel:释放Try阶段预留的业务资源;Cancel操作满足幂等性

2. 业务流程:

(1)第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的
try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段

(2)第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则
活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作

3.与2pc比较

1)位于业务服务层而非资源层

2)没有单独的准备(prepare)阶段,Try操作兼备资源操作与准备能力

3)Try操作可以灵活选择业务资源的锁定粒度

4)开发成本较高

5)XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。 TCC是业务层面的分布式事务,
最终一致性,不会一直持有资源的锁

4.缺点

1)Canfirm和Cancel的幂等性很难保证

2)这种方式缺点比较多,通常在复杂场景下是不推荐使用的,除非是非常简单的场景,非常容易提供回滚Cancel,而且依赖
的服务也非常少的情况

3)这种实现方式会造成代码量庞大,耦合性高。而且非常有局限性,因为有很多的业务是无法很简单的实现回滚的,如果串行
的服务很多,回滚的成本实在太高	

其核心在于将业务分为两个操作步骤完成。不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
下面还是以银行转账例子来说明
假设用户user表中有两个字段:可用余额(available_money)、冻结余额(frozen_money)
A扣钱对应服务A(ServiceA)
B加钱对应服务B(ServiceB)
转账订单服务(OrderService)
业务转账方法服务(BusinessService)
ServiceA,ServiceB,OrderService都需分别实现try(),confirm(),cancle()方法,方法对应业务逻辑如下
本地事务&分布式事务_第6张图片
其中业务调用方BusinessService中就需要调用
ServiceA.try()
ServiceB.try()
OrderService.try()
1、当所有try()方法均执行成功时,对全局事物进行提交,即由事物管理器调用每个微服务的confirm()方法
2、 当任意一个方法try()失败(预留资源不足,抑或网络异常,代码异常等任何异常),由事物管理器调用每个微服务的cancle()方法对全局事务进行回滚
引用网上一张TCC原理的参考图片
本地事务&分布式事务_第7张图片

TCC注意问题

幂等问题

空回滚

事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;
TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行;

防悬挂

事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;
用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求;
本地事务&分布式事务_第8张图片
可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段回滚操作已经执行,不再执行try方法;

优缺点比较

本地事务&分布式事务_第9张图片

本地信息表(异步确保一致性)(可靠事件模式)

本地事务&分布式事务_第10张图片
1.基本思路

1)消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个
数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。

2)消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失
败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作

3)生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,
这种方案还是非常实用的

2.优点

1)一种非常经典的实现,避免了分布式事务,实现了最终一致性

3. 缺点

1)消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理

MQ事务消息(可靠事件模式)

有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持
本地事务&分布式事务_第11张图片
1. 思路:

1)首先,发送一个事务消息,这个时候,RocketMQ将消息状态标记为Prepared,注意此时这条消息消费者是无法消费到的

2)接着,执行业务代码逻辑,可能是一个本地数据库事务操作

3)最后,确认发送消息,这个时候,RocketMQ将消息状态标记为可消费,这个时候消费者,才能真正的保证消费到这条数据

如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息
发送端(生产者)确认。RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送
与本地事务同时成功或同时失败

2. 优点:

(1)实现了最终一致性,不需要依赖本地数据库事务

3.缺点

(1)实现难度大,主流MQ不支持,没有.NET客户端,RocketMQ事务消息部分代码也未开源	

MQ非事务消息(加独立消息服务、或者本地事务表)

另外一种实现,并不是所有的mq都支持事务消息。也就是消息一旦发送到消息队列中,消费者立马就可以消费到。此时可以使用独立消息服务、或者本地事务表
本地事务&分布式事务_第12张图片
1.思路

1)将消息先发送到一个我们自己编写的一个"独立消息服务"应用中,刚开始处于prepare状态

2)业务逻辑处理成功后,确认发送消息,这个时候"独立消息服务"才会真正的把消息发送给消息队列

3)消费者消费成功后,ack时,除了对消息队列进行ack(图中没有画出),对于独立消息服务也要进行ack,
"独立消息服务"一般是把这条消息删除。而定时扫描prepare状态的消息,向消息发送端(生产者)确认的工作也由独立消息
服务来完成

2.对于"本地事务表"

 对于"本地事务表",其实和"独立消息服务"的作用类似,只不过"独立消息服务"是需要独立部署的,
 而"本地事务表"是将"独立消息服务"的功能内嵌到应用中

柔性事务:最大努力通知(补偿?)

最大努力通知型( Best-effort delivery)是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果 不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。最大努力通知型的实现方案,一般符合以下特点:
本地事务&分布式事务_第13张图片

  1. 不可靠消息:业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次后不再通知,允许消息丢失(不可靠消息)
  2. 定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息

Sagas长事务

在Sagas事务模型中,一个长事务是由一个预先定义好执行顺序的子事务集合和他们对应的补偿子事务集合组成的。典型的一个完整的交易由T1、T2、…、Tn等多个业务活动组成,每个业务活动可以是本地操作、或者是远程操作,所有的业务活动在Sagas事务下要么全部成功,要么全部回滚,不存在中间状态
本地事务&分布式事务_第14张图片
1.实现机制

1)每个业务活动都是一个原子操作

2)每个业务活动均提供正反操作

3)任何一个业务活动发生错误,按照执行的反顺序,实时执行反操作,进行事务回滚

4)回滚失败情况下,需要记录待冲正事务日志,通过重试策略进行重试

5)冲正重试依然失败的场景,提供定时冲正服务器,对回滚失败的业务进行定时冲正

6)定时冲正依然失败的业务,等待人工干预

2.优点

1)Sagas长事务模型支持对数据一致性要求比较高的场景比较适用,由于采用了补偿的机制,每个原子操作都是先执行任务,
避免了长时间的资源锁定,能做到实时释放资源,性能相对有保障。

3. 缺点

1)Sagas长事务方式如果由业务去实现,复杂度与难度并存

4. Sagas长事务模型框架
本地事务&分布式事务_第15张图片

Seata事务

解决分布式事务问题,有两个设计初衷

● 对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
● 高性能:减少分布式事务解决方案所带来的性能消耗

seata中有两种分布式事务实现方案,AT及TCC

● AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题
● TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题

AT模式(业务侵入小)

Seata AT模式是基于XA事务演进而来的一个分布式事务中间件,XA是一个基于数据库实现的分布式事务协议,本质上和两阶段提交一样,需要数据库支持,Mysql5.6以上版本支持XA协议,其他数据库如Oracle,DB2也实现了XA接口

角色如下
本地事务&分布式事务_第16张图片

  • Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
  • Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
  • Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
    基本处理逻辑如下
    本地事务&分布式事务_第17张图片
    Branch就是指的分布式事务中每个独立的本地局部事务
    第一阶段
    Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。

这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在
本地事务&分布式事务_第18张图片
基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源

这也是Seata和XA事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的

同时Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果

第二阶段
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成
本地事务&分布式事务_第19张图片如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚
本地事务&分布式事务_第20张图片

TCC(高性能)

seata也针对TCC做了适配兼容,支持TCC事务方案,原理前面已经介绍过,基本思路就是使用侵入业务上的补偿及事务管理器的协调来达到全局事务的一起提交及回滚,详情参考demo回滚
本地事务&分布式事务_第21张图片

原理

Seata 的设计思路是将一个分布式事务可以理解成一个全局事务,下面挂了若干个分支事务,而一个分支事务是一个满足 ACID 的本地事务,因此我们可以操作分布式事务像操作本地事务一样。

Seata 内部定义了 3个模块来处理全局事务和分支事务的关系和处理过程,这三个组件分别是:

Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
本地事务&分布式事务_第22张图片

简要说说整个全局事务的执行步骤:

● TM 向 TC 申请开启一个全局事务,TC 创建全局事务后返回全局唯一的 XID,XID 会在全局事务的上下文中传播;
● RM 向 TC 注册分支事务,该分支事务归属于拥有相同 XID 的全局事务;
● TM 向 TC 发起全局提交或回滚;
● TC 调度 XID 下的分支事务完成提交或者回滚。

Zookeeper事务

Zookeeper通过ZAB保证分布式事务的最终一致性。

ZAB全称Zookeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议)

  • ZAB是一种专门为Zookeeper设计的一种支持 崩溃恢复 的 原子广播协议 ,是Zookeeper保证数据一致性的核心算法。ZAB借鉴了Paxos算法,但它不是通用的一致性算法,是特别为Zookeeper设计的。
  • 基于ZAB协议,Zookeeper实现了⼀种主备模式的系统架构来保持集群中各副本之间的数据的⼀致性,表现形式就是使⽤⼀个单⼀的主进程(Leader服务器)来接收并处理客户端的所有事务请求(写请求),并采⽤ZAB的原⼦⼴播协议,将服务器数据的状态变更为事务 Proposal的形式⼴播到所有的Follower进程中。

问题提出

  • 主从架构下,leader 崩溃,数据一致性怎么保证?
  • 选举 leader 的时候,整个集群无法处理写请求的,如何快速进行 leader 选举?

ZAB过程

ZAB协议的核⼼是:定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理⽅式
本地事务&分布式事务_第23张图片
所有事务必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器

  • Leader服务器负责将一个客户端事务请求转化为一个事务Proposal(提案),并将该Proposal分发给集群中所有的Follower服务器
  • Leader服务器等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,Leader就会向所有的Follower服务器发送Commit消息,要求将前一个Proposal进行提交。

ZAB协议内容简介

ZAB协议包括两种基本的模式:崩溃恢复消息广播

消息广播

当集群中有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入 消息广播模式

当一台遵守ZAB协议的服务器启动后加入到集群中,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么加入的服务器会自觉的进入数据恢复模式:找到Leader 所在的服务器,并与其进⾏数据同步,数据同步完成后参与到消息⼴播流程中。

ZAB协议的消息广播使用原子广播协议,类似一个二阶段提交的过程 ,但又有所不同。

  • 二阶段提交中,需要所有参与者反馈ACK后再发送Commit请求。要求所有参与者要么成功,要么失败。这样会产生严重的阻塞问题
  • ZAB协议中,Leader等待半数以上的Follower成功反馈ACK即可,不需要收到全部的Follower反馈ACK。
消息广播过程:
  • 客户端发起写请求
  • Leader将客户端请求信息转化为事务Proposal,同时为每个Proposal分配一个事务ID(Zxid)
  • Leader为每个Follower单独分配一个FIFO的队列,将需要广播的Proposal依次放入到队列中
  • Follower接收到Proposal后,首先将其以事务日志的方式写入到本地磁盘中,写入成功后给Leader反馈一个ACK响应
  • Leader接收到半数以上Follower的ACK响应后,即认为消息发送成功,可以发送Commit消息
  • Leader向所有Follower广播Commit消息,同时自身也会完成事务提交。Follower接收到Commit消息后也会完成事务的提交
    本地事务&分布式事务_第24张图片

崩溃恢复

在整个服务框架启动过程中,如果Leader服务器出现网络中断、崩溃退出或重启等异常情况,ZAB协议就会进入崩溃恢复模式。同时选举出新的Leader服务器。

当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步(数据同步)之后,ZAB协议会退出恢复模式。

  • 在ZAB协议中,为了保证程序的正确运⾏,整个恢复过程结束后需要选举出⼀个新的Leader 服务器。
  • Leader选举算法不仅仅需要让Leader⾃身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产⽣出来的新Leader服务器。

ZAB保证数据一致性

ZAB协议规定了 如果⼀个事务Proposal在⼀台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。 针对这些情况ZAB协议需要保证以下条件:

  • 已经在Leader服务器上提交的事务最终被所有服务器都提交。
    假设⼀个事务在 Leader 服务器上被提交了,并且已经得到过半 Folower 服务器的Ack反馈,但是在它 将Commit消息发送给所有Follower机器之前,Leader服务器挂了

  • 丢弃只在Leader服务器上被提出(未提交)的事务。
    假设初始的 Leader 服务器 Server1 在提出了⼀个事务Proposal3 之后就崩溃退出 了,从⽽导致集群中的其他服务器都没有收到这个事务Proposal3。于是,当 Server1 恢复过来再次加 ⼊到集群中的时候,ZAB 协议需要确保丢弃Proposal3这个事务。

综上所述,ZAB的选举出来的Leader必须满足以下条件:

能够确保提交已经被 Leader 提交的事务 Proposal,同时丢弃已经被跳过的事务 Proposal。即:

  • 新选举出来的 Leader 不能包含未提交的 Proposal。
  • 新选举的 Leader 节点中含有最大的 zxid 。

ZAB如何数据同步

所有正常运行的服务器要么成为Leader,要么成为Follower并和Leader保持同步。

  • 完成Leader选举(新的 Leader 具有最高的zxid)之后,在正式开始⼯作(接收客户端请求)之前,Leader服务器会⾸先确认事务⽇志中的所有Proposal是否都已经被集群中过半的机器提交了,即 是否完成数据同步
  • Leader服务器需要确保所有的Follower服务器能够接收到每⼀条事务Proposal,并且能够正确地将所有已经提交了的事务Proposal应⽤到内存数据中。等到 Follower服务器将所有其尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并成功应⽤到本地数据库中后,Leader服务器就会将该Follower服务器加⼊到真正的可⽤Follower列表中,并开始之后的其他流程。

ZAB运行时状态#

ZAB协议设计中,每个进程都有可能处于如下三种状态之一:

  • LOOKING:Leader选举状态,正在寻找Leader
  • FOLLOWING:当前节点是Follower。与Leader服务器保持同步状态
  • LEADING:当前节点是Leader,作为主进程领导状态。

ZAB状态的切换

启动时的状态转换

  • 所有进程的初始状态都是LOOKING状态,此时不存在Leader。
  • 接下来,进程会试图选举出来一个新的Leader,Leader切换为LEADING状态,其它进程发现已经选举出新的Leader,那么它就会切换到FOLLOWING状态,并开始与Leader保持同步。
  • 处于FOLLOWING状态的进程称为Follower,LEADING状态的进程称为Leader。
  • 当Leader崩溃或者放弃领导地位时,其余的Follower进程就会切换到LOOKING状态开始新一轮的Leader选举。

运行过程中的状态转换

一个Follower只能和一个Leader保持同步,Leader进程和所有的Follower进程之间通过心跳监测机制来感知彼此的情况。

  • 若Leader能够在超时时间内正常的收到心跳检测,那么Follower就会一直与该Leader保持连接。
  • 如果在指定时间内Leader无法从过半的Follower进程那里接收到心跳检测,或者TCP连接断开,那么Leader会放弃当前周期的领导,并转换为LOOKING状态;其他的Follower也会选择放弃这个Leader,同时转换为LOOKING状态,之后会进行新一轮的Leader选举

ZAB的四个阶段

选举阶段(Leader Election)

节点在一开始都处于选举阶段,只要有一个节点超过半数阶段的票数,它就可以当选准Leader,只有到达第三个阶段(同步阶段),这个准Leader才会成为真正的Leader。另外,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 Zookeeper 系列面试题和答案,非常齐全。

这一阶段的目的就是为了选出一个准Leader,然后进入下一阶段。

发现阶段

在这个阶段中,Followers和上一轮选举出的准Leader进行通信,同步Followers最近接受的事务Proposal。这个阶段主要目的是发现当前大多数节点接受的最新提议,并且准Leader生成新的epoch,让Followers接受,更新它们的acceptedEpoch。

一个Follower只会连接一个Leader,如果有一个节点F认为另一个Follower P是Leader,F在尝试连接P时会被拒绝,F被拒绝后,就会进入选举阶段。

ZAB-发现阶段:
本地事务&分布式事务_第25张图片

同步阶段

同步阶段主要是利用 Leader 前一阶段获得的最新 Proposal 历史,同步集群中所有的副本。

只有当 quorum(超过半数的节点) 都同步完成,准 Leader 才会成为真正的 Leader。Follower 只会接收 zxid 比自己 lastZxid 大的 Proposal。

ZAB同步阶段:
本地事务&分布式事务_第26张图片

广播阶段

到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 Leader 可以进行消息广播。同时,如果有新的节点加入,还需要对新节点进行同步。需要注意的是,Zab 提交事务并不像 2PC 一样需要全部 Follower 都 Ack,只需要得到 quorum(超过半数的节点)的Ack 就可以。

ZAB广播阶段:
本地事务&分布式事务_第27张图片

ZAB协议实现

Java 版本的ZAB协议的实现跟上面的定义略有不同,选举阶段使用的是 Fast Leader Election(FLE),它包含了步骤2的发现职责。因为FLE会选举拥有最新提议的历史节点作为 Leader,这样就省去了发现最新提议的步骤。

实际的实现将 发现和同步阶段合并为 Recovery Phase(恢复阶段) ,所以,Zab 的实现实际上有三个阶段。

快速选举(Fast Leader Election)

前面提到的 FLE 会选举拥有最新Proposal history (lastZxid最大)的节点作为 Leader,这样就省去了发现最新提议的步骤。 这是基于拥有最新提议的节点也拥有最新的提交记录

成为Leader的条件:

  • 选epoch最大的
  • epoch相等,选zxid最大的
  • epoch和zxid都相等,选server_id最大的(zoo.cfg 中配置的 myid)

节点在选举开始时,都默认投票给自己,当接收其他节点的选票时,会根据上面的 Leader条件 判断并且更改自己的选票,然后重新发送选票给其他节点。当有一个节点的得票超过半数,该节点会设置自己的状态为 Leading ,其他节点会设置自己的状态为 Following。
本地事务&分布式事务_第28张图片

恢复阶段(Recovery Phase)

这一阶段 Follower 发送他们的 lastZxid 给 Leader,Leader 根据 lastZxid 决定如何同步数据。这里的实现跟前面的 阶段 3 有所不同:Follower 收到 TRUNC 指令会终止 L.lastCommitedZxid 之后的 Proposal ,收到 DIFF 指令会接收新的 Proposal。

history.lastCommittedZxid:最近被提交的提议的 zxid history.oldThreshold:被认为已经太旧的已提交提议的 zxid

本地事务&分布式事务_第29张图片

广播阶段(Broadcast Phase)

参考 4.1 [ZAB协议内容#消息广播]

ZAB与Paxos的联系和区别

联系

  • 都存在一个类似Leader进程的角色,由其负责协调多个Follower进程的运行
  • Leader进程都会等待超过半数的Follower作出正确的反馈后,才会将一个提议进行提交(过半原则)
  • 在ZAB中,每个Proposal中都包含了一个epoch值,用来代表当前Leader周期,在Paxos中同样存在这样的一个表示,名字为 Ballot。

区别

  • Paxos算法中,新选举产生的主进程会进行两个阶段的工作;第一阶段称为读阶段:新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第二阶段称为写阶段:当前主进程开始提出自己的提议。
  • ZAB协议在Paxos基础上添加了同步阶段,此时,新的Leader会确保存在过半的Follower已经提交了之前Leader周期中的所有事物Proposal。这一同步阶段的引入,能够有效保证,Leader在新的周期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal的提交。

总的来说,ZAB协议和Paxos算法的本质区别在于两者的设计目的不一样:ZAB协议主要用于构建一个高可用的分布式数据主备系统,而Paxos算法则用于构建一个分布式的一致性状态机系统。

另外,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 Zookeeper 系列面试题和答案,非常齐全。

总结

问题解答:

  • 主从架构下,leader 崩溃,数据一致性怎么保证?

  • leader 崩溃之后,集群会选出新的 leader,然后就会进入恢复阶段,新的 leader 具有所有已经提交的提议,因此它会保证让 followers 同步已提交的提议,丢弃未提交的提议(以 leader 的记录为准),这就保证了整个集群的数据一致性。

  • 选举 leader 的时候,整个集群无法处理写请求的,如何快速进行 leader 选举?
    这是通过 Fast Leader Election 实现的,leader 的选举只需要超过半数的节点投票即可,这样不需要等待所有节点的选票,能够尽早选出 leader。

参考网址

7.0 柔性事务:可靠消息最终一致性(一系列)

分布式事务:不过是在一致性、吞吐量和复杂度之间,做一个选择

分布式事务之一:整体介绍

聊聊分布式事务,再说说解决方案

分布式事务一(总结的一些列文章,很好,待读)

分布式事务解决办法

MySQL分布式事务(新鲜玩法,待读)

分布式系统数据一致性的6种方案(几个公司的解决方案,待读)

你可能感兴趣的:(分布式事务,java)