分布式事务
- 2PC
- 3PC
- TCC
- 事务消息
2PC
问题🤔️:为什么二阶段提交协议要引入协调者?
在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有参与者节点的操作结果,并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。
问题🤔️:两阶段提交协议成立的前提假设是什么?
- 该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Participants),且节点之间可以进行网络通信。
- 所有节点都采用WAL预写式日志,并且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的丢失。
- 所有节点不会永久性损坏,即使损坏后仍然可以恢复。
问题🤔️:什么是WAL预写式日志?
预写式日志(Write-ahead logging,缩写 WAL)是关系数据库系统中用于提供原子性和持久性(ACID属性中的两个)的一系列技术。在使用WAL的系统中,所有的修改在提交之前都要先写入日志文件中。日志文件中通常包括redo和undo信息,这样做的目的可以通过一个例子来说明:假设一个程序在执行某些操作的过程中机器掉电了。在重新启动时,程序可能需要知道当时执行的操作是成功了还是部分成功或者是失败了。如果使用了WAL,程序就可以检查日志文件,并对突然掉电时计划执行的操作内容跟实际上执行的操作内容进行比较。在这个比较的基础上,程序就可以决定是撤销已做的操作还是继续完成已做的操作,或者是保持原样。
问题🤔️:两阶段提交协议有什么缺点?
二阶段提交算法的最大缺点就在于它的执行过程中间,参与者节点在收到协调者发出的commit/abort命令之前都处于阻塞状态。因为参与者节只有等到事务提交/回滚后,才会释放在资源上的锁,这将导致系统的吞吐量显著下降。
3PC
TCC
TCC 分为三个阶段 try - confirm - cancel,每个业务都需要提供有这三个方法的实现。TCC 类似于 2PC,最大的不同在于不依赖于数据库的锁实现资源隔离,而是通过参与者在业务层面实现资源隔离。
问题🤔️:TCC协议有什么缺点?
TCC 对业务的耦合性很大,业务上需要做一定的改造才能完成这三个方法,这其实就是 TCC 的缺点,并且 confirm 和 cancel 操作要注意幂等。
事务消息
事务消息解决消息投递与本地事务的一致性问题,主要是适用于异步更新的场景,并且对数据实时性要求不高的地方。
本地事务的执行可能成功提交也可能失败回滚,
消息投递置于事务后
// 开始事务
try {
// 1.执行数据库操作
// 2.提交事务
} catch (Exception e){
// 3.回滚事务
}
// 4.发送 mq 消息
缺点:如果事务提交成功,但是 mq 消息发送失败,则会存在一致性问题;
消息投递置于事务中
// 开始事务
try {
// 1.执行数据库操作
// 2.发送 mq 消息
// 3.提交事务
} catch (Exception e){
// 4.回滚事务
}
缺点:可能存在消息已经发送到 MQ 服务端,但是由于网络问题未及时收到 MQ 的ACK响应,导致消息发送端认为消息发送失败而回滚本地事务。但是实际上消息发送成功,则也会存在一致性问题;即使消息发送可以通过增加重试次数重新发送消息,但是依然无法保证一定能收到 MQ 的 ACK 响应,另外重试会拉长数据库事务执行时间,导致事务中锁的持有时间变长,影响整体的数据库吞吐量。
RocketMQ 事务消息
RocketMQ 解决的是本地事务的执行和发消息这两个动作满足事务的约束。
RocketMQ 的事务消息也可以被认为是一个两阶段提交,简单的说就是在事务开始的时候会先发送一个半消息给 Broker。半消息的意思就是这个消息此时对 Consumer 是不可见的,而且也不是存在真正要发送的队列中,而是一个特殊队列。发送完半消息之后再执行本地事务,再根据本地事务的执行结果来决定是向 Broker 发送提交消息,还是发送回滚消息。
问题🤔️:如果向 Broker 发送提交或者回滚消息失败了怎么办?
Broker 会定时的向 Producer 来反查这个事务是否成功,具体的就是 Producer 需要暴露一个接口,通过这个接口 Broker 可以得知事务到底有没有执行成功,没成功就返回未知,因为有可能事务还在执行,会进行多次查询。
Kafka 事务消息
Kafka 事务消息则是用在一次事务中需要发送多个消息的情况,保证多个消息之间的事务约束,即多条消息要么都发送成功,要么都发送失败。 Kafka 的事务有事务协调者角色,事务协调者其实就是 Broker 的一部分。
在开始事务的时候,生产者会向事务协调者发起请求表示事务开启,事务协调者会将这个消息记录到特殊的日志-事务日志中,然后生产者再发送真正想要发送的消息,这里 Kafka 和 RocketMQ 处理不一样,Kafka 会像对待正常消息一样处理这些事务消息,由消费端来过滤这个消息。
然后发送完毕之后生产者会向事务协调者发送提交或者回滚请求,由事务协调者来进行两阶段提交,如果是提交那么会先执行预提交,即把事务的状态置为预提交然后写入事务日志,然后再向所有事务有关的分区写入一条类似事务结束的消息,这样消费端消费到这个消息的时候就知道事务好了,可以把消息放出来了。
最后协调者会向事务日志中再记一条事务结束信息,至此 Kafka 事务就完成了