在分布式系统中,我们一般会根据业务对服务进行划分,这就会存在一些服务间的调用关系,所以普通的本地事务是满足不了我们的要求了,需要引入分布式事务来实现我们的一致性的要求。
这里对事务进行一个简单的介绍:
同学A给同学B转250块钱,系统首先从同学A的银行卡扣除250块钱,如果上述这个步骤成功了,但是系统往同学B的银行卡上转250块钱的时候失败了,那么就会造成数据对不上的情况了。在这种情况下我们就需要引入事务,保证转钱这两步操作要么同时成功要么同时失败,成功的情况就是同学B银行卡也收到了钱,失败的情况是回滚把钱退回给A。
我们先从强一致性的场景说起,我们先了解一下 ACID 理论:
- Atomicity:原子性(要么全部执行,要么全部都不执行)
- Consistency:一致性(数据库只有一个状态,不存在未确定状态)
- Isolation:隔离性(事务之间互不干扰)
- Durability: 永久性(事务一旦提交,数据库记录永久不变)
分布式事务实现方案也比较多(比如两阶段提交、三阶段提交、TCC等),下面我们对它们进行一些简单的介绍
2PC:两阶段提交
从名字我们可以理解到,2PC就是有两个阶段,一个是准备阶段,一个是提交阶段
Prepare阶段:协调者发起提议,问大家是否接受,这个阶段做了除提交事务外的所有事情。
Commit阶段:根据参与者的反馈,通知大家提交或者终止事务,如果参与者全部同意就提交,只要有一个参与者不同意就终止。
2PC能协调参与者统一提交或者终止,按道理是能实现我们需要的分布式事务的,但是我们需要考虑网络波动以及协调者挂掉的情况,在这些情况下2PC是会存在一些问题的。
单点故障:在上图可以看得出协调者是一个非常重要的角色,一旦协调者发生故障,那么参与者会一直阻塞下去,无法完成事务操作。
性能问题:无论是上面的第一个阶段还是第二个阶段,所有的参与者和协调者的资源都是被锁定的,要等所有参与者准备完毕,协调者才会通知进行全局提交。
数据不一致:在第二阶段的时候,有的参与者接受到commit请求后,发生了网络波动,导致部分参与者接收到commit请求但是部分没有接受到,导致当接受到命令的参与者执行命令后导致数据不一致的问题。
3PC:三阶段提交
三阶段提交是二阶段提交的升级版,改动点如下
1.引入超时机制
2.在第一阶段和第二阶段中间插入了一个准备阶段,保证了在最后提交阶段前各节点的状态一致。
阶段一:canCommit: 协调者向所有的参与者询问是否可以提交事务,参与者接到请求后根据自己的情况返回yes或者是no。
阶段二:preCommit: 协调者根据第一阶段参与者反馈的情况判断是否可以执行基于事务的preCommit操作(如果阶段一全部返回yes则向所有参与者发出preCommit请求,进入准备阶段,参与者接受到请求后,执行事务操作,将undo和redo信息记录到事务日志中,但是不提交事务,然后参与者向协调者反馈ack响应或者no响应),这里做了除提交事务外的所有事情。
阶段三:do Commit: 真正提交事务的阶段,如果阶段二中所有的参与者返回的都是ack,那么协调者会向参与者发出do commit请求,参与者收到请求后会执行事务提交,完成后向发起者反馈ack消息;如果阶段二中只要有一个参与者反馈no或者是超时后协调者无法收到所有参与者的反馈,即中断事务,中断是由协调者向参与者发出abort请求,参与者使用阶段一的undo信息执行回滚操作,操作完成后向协调者返回ack。
当进入到阶段三,无论是协调者出现问题,或者是协调者和参与者网络出现了问题,都可能会导致参与者无法接收到协调者发出的do Commit请求或者abort请求,此时参与者都会在超时后自动提交事务。
总结:虽然相对于2PC进行了一些优化,不会在准备阶段就直接锁住资源;但因为3PC毕竟多引入了一个阶段,所以相对于2PC性能会更差,而且依然会存在数据不一致的问题。
这里引入CAP理论和BASE理论
CAP是指在一个分布式系统下, 包含三个要素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),并且 三者不可得兼。
C:Consistency,一致性,所有数据变动都是同步的。
A:Availability,可用性,即在可以接受的时间范围内正确地响应用户请求。
P:Partition tolerance,分区容错性,即某节点或网络分区故障时,系统仍能够提供满足一致性和可用性的服务
BASE理论主要是解决 CAP 理论中分布式系统的可用性和一致性不可兼得的问题。BASE 理论包含以下三个要素:
BA:Basically Available,基本可用。
S:Soft State,软状态,状态可以有一段时间不同步。
E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致。
BASE 模型与 ACID 不同,满足 CAP 理论,通过 牺牲强一致性来保证系统可用性。由于牺牲了强一致性,系统在处理请求的过程中,数据可以存在短时的不一致情况。
TCC(Try-Confirm-Cancel)
一阶段Try行为:
调用方会分别调用多个被调用方,只有当多个被调用方都返回成功的时候,才会执行confirm操作,否则就执行cancel,这个阶段其实就是一个检查资源和锁定资源的操作,在真正执行前的一个冻结操作。
二阶段Confirm行为:
使用预留的资源,完成真正的业务操作,要求Try成功Confirm一定也是要成功的。要是个别或者全部confirm失败了或者调用超时了,和try阶段不同的是在confirm阶段不会做回滚操作,而是将所有的confirm进行重试,如果超过重试次数,则必须告警通知人工进行处理。
二阶段Cancel行为:
这个阶段是释放预留资源的阶段,只有在try阶段失败或者异常的情况下才会执行cancel操作。而且这里只对try步骤中已经成功的被调用者进行cannel处理,同时这个步骤失败也会重试。
TCC是柔性事务,解决了2PC中全局资源锁定导致效率低下的问题。
基于MQ的最终一致性
比如用户购买了一件商品,这个时候订单服务要告知商品服务给其销售量+1,这个时候我们就可以通过MQ进行消息的通知,就算下游宕机了,等其重新启动后也能正常接收到消息,保证状态的最终一致性,但需要容忍数据暂时不一致的情况。
但是其实我们要保证MQ的可靠性,例如使用RabbitMQ的时候其实是有两种方式可以保证其可靠性,一个是基于AMQP的事务,一个是基于Confirm模式,因为考虑到事务对MQ性能影响比较大,不太符合我们用分布式系统的要求,所以下面主要介绍通过Confirm模式保证MQ的可靠性。
确保生产者将数据投递到MQ服务器中:
生产者将信道设置成confirm模式,一旦信道进入confirm模式,在该信道上面发布的消息都会被指派一个唯一的ID,消息被投递到所匹配的队列后,Broker就会发送一个确认给生产者,生产者就可以知道消息是否已经被投递,如果失败可以进行重试操作。
确保消息持久化:
消息是可以被持久化到磁盘的,这样即使是rabbitmq宕机数据也不会丢失。
确保消费者能正常消费:
消费者启用ack模式,如果没有收到消费者返回的ack,会重新安排消息重新进入队列。
基于MQ的方式其效率非常,但是其也是会有短时的状态不同步的问题,失败的时候会自动重试并且当重试次数到达指定值后还要考虑人工进行干预。
注意
因为上述的实现方案中其实会有重试的情况,所以我们要保证接口的幂等性,防止同一个操作执行成功两次。
原文链接: https://juejin.cn/post/6912462666187407367
如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!