网上已经有相当多的文章有在讲这个了,不过我还是倾向于自己做一个笔记,将自己的想法加进去,加深对它的理解。
分布式事务
当我们的系统为一个单机系统的时候,我们要同时更新多张数据库表,那么就可以直接使用mysql的本地事务,它能够保证我们在这个事务中的数据库操作类似一个原子操作,要么全部成功,要么全部失败,那么就能够保证其一致性。但是当一个系统中有划分为多个微服务的时候,可能一个操作需要去远程调用多个服务之间的接口,并且这些数据也都分布在不同的数据库中,而这个时候为了去保证它的正确性,还想原来一样的使用本地事务肯定就不行了,这就涉及到了分布式事务。
1. 基于XA协议的2PC和3PC
2PC
自从MySQL5.5以后,其数据库就自带了基于XA协议的二阶段提交,它是一个强一致性的方案。
二阶段提交顾名思义就是分为两个阶段提交:
- 准备阶段
事务协调者向各参与者发起prepare请求,参与者在接收到该请求后,开始将数据的redolog和undolog写入,然后再向协调者反馈成功还是失败。 - 提交阶段
协调者在向各参与者发起prepare请求以后,就会等待各个参与者的反馈,当收到所有的参与者反馈成功以后,就又会向各个参与者发起提交请求。
如果参与者反馈失败或者反馈超时,那么协调者会向各参与者发起回滚请求。
参与者在接收到提交或者回滚请求后,就会对数据进行提交或回滚操作,释放在事务期间锁占用的资源,并向协调者反馈成功。
在二阶段提交中,存在着许多的缺点:
- 性能低下的同步阻塞
由于在二阶段提交的整个过程中,一直是在一个同步阻塞的状态,并且所占用的资源也得一直等到第二阶段结束之后才进行释放,所以就造就了这个方案的性能问题。 - 单点问题
在这个方案中,协调者(事务管理器)处于一个至关重要的作用,如果他宕机了,那么就会直接导致分布式事务出现异常。比如在第一阶段已经完成,各参与者(资源管理器)在等待协调器提交请求的时候协调器突然宕机了,那么就会导致各个参与者一直在等待的过程中,而其占用的资源将会一直得不到释放。 - 数据不一致问题
在第二阶段的时候,协调者向各参与者发送提交请求时,如果因为网络问题,有参与者没有接收到这个提交请求,那么这个参与者的数据便不会得到提交,并一直占用着资源。 - 协调者与某一参与者同时宕机问题
当协调者在第二阶段往各参与者发送提交请求的时候,协调者和唯一接收到这个请求的参与者同时宕机,即使通过重新选举协调者,以后也无法再之前当前事务的状态了。
3PC
3PC是基于二阶段提交的一小部分改进,引入了参与者的超时机制,使得当协调者挂在了提交阶段的时候,参与者超时自动提交,然而这就有可能又会造成数据的不一致,可能协调者是想要你回滚的,然而你却提交了。
3PC的三个阶段:
- 准备阶段
这个准备阶段不同于二阶段提交中的准备阶段,不会一上来就直接锁定资源开始干活这样的,而是先会去判断各个参与者的状态,如果参与者反馈可以进行事务操作的话,就会开始下面的两个阶段。 - 预提交阶段
协调者向参与者发送预提交请求,参与者开始执行事务操作,并反馈状态。
当有参与者处于预提交阶段的时候,就说明所有的参与者都处在了这个预提交阶段,如果是在预提交阶段之前参与者等待超时,那就直接失败了,因为在这之前本就啥也没干。 - 提交阶段
如果已经到了提交阶段,在等待协调者的提交请求超时,那就直接提交,所以说可能就会因为在这里导致数据的不一致,原来要的是回滚这里却默认超时自动提交了。
可以看到,无论是2PC还是3PC,都无法很好的解决分布式事务的问题,并且由于强一致性的特点还带有着性能问题。
2. 基于业务层的TCC
上面的两个2PC和3PC的方案都是基于数据库层面而实现的,因此实际的业务程序中并不会有什么大的改造。而TCC便是基于业务层面的分布式事务解决方案。并且除了上面的这个方案是强一致性的以外,以下的这三种方案都是基于base理论的柔性事务,实现的是最终一致性。
TCC就是Try-Confirm-Cancel的简称。
- Try:在这里会完成业务检查和进行资源的提前预留。
- Confirm:这里不会进行业务检查,直接使用预留的资源执行业务操作。
- Cancel:取消Try阶段所占用的业务资源。
比如这样一个场景:去买一个需要坐两趟飞机中转的飞机票,那么try阶段就先向航空公司申请锁定这两张飞机票,try阶段这些预留资源都成功了,就开始执行confirm操作,购买这两张机票,如果try阶段预留失败则cancel解锁预留的机票。
TCC相对于XA协议来说,不会有长事务,每个阶段都会有单独的事务,执行完就直接提交了,这样就不会造成较长时间的数据库层面的资源锁定了,因为它try锁定资源是从业务层面去将资源锁定的。
3. 最大努力通知
最大努力通知时一种简单的柔性事务,它适用于对最终一致性时间敏感度低的场景。
一般来说可以通过mq来实现最大努力通知,消息的通知方向mq中发送消息,而消息的接收方可以监听该消息队列。在这个方案中要实现以下几点:
- 消息的通知方通过mq的ack机制判断消息有没有正常的发出去,并设置一个最大的重试次数,没有正常发出去需要通过一定的重试机制来进行重新发送消息,尽最大的努力将消息成功的发送出去。
- 所以有了消息重复发送机制,那么这个消息的接收方一定要对消息有幂等性处理。
- 即使有了消息的重复发送机制,消息仍然可能不能正确的抵达消息接收方,因此,消息接收方需要提供一个查询接口,在消息接收方可以定期的,或者在需要的时候,通过这个查询接口来保证查到该消息,恢复没有正常发送过来的消息,实现最终一致性。
比如银行通知、商户通知,或者是支付宝的支付成功的异步回调等。
4. 可靠消息实现最终一致性
这个方案已定要保证的就是消息的可靠性,消息一定要成功的发出去,并让消费者消费。以rabbitmq为例,使用它来做可靠消息实现最终一致性,需要保证下面的几个点:
- 消息落库
在发送消息前先将消息持久化到数据库,并设置当前状态为已发送。 - 消息队列的消息持久化机制
防止因为消息队列宕机而导致的消息丢失的情况,消息队列每接受到一个消息都必须要将其持久化到磁盘,才能够算成功接收。 - 消息生产者的消息到交换机确认,消息到队列失败回调,以及网络断开api直接报错的处理。
需要在配置中开启发送到交换机的确认以及路由队列失败后的回调,并定义好confirm方法和returnedMessage方法。
消息发送成功则会对原消息表中的数据状态变更为已接收。在消息发送失败的时候采取一定的重试机制重新发送,在重试到达最大次数以后不再处理。 - 消息消费者的手动确认
消费者在取出消息后,必须要在业务逻辑执行完成以后,手动的对消息队列进行basicAck确认,防止消息刚取出来还没来得及执行业务逻辑,就直接挂了,这就相当于是消息白跑一趟。如果是有手动进行ack确认的,即使挂了,只要没进行确认,消息就不会丢失,可以再次从消息队列拿到该消息。
在执行完业务逻辑后还需要修改消息表中该消息的状态为已经消费。 - 定时任务
定时的对消息表中的发送消息在五分钟前且状态为未成功消费的消息进行重新发送,并设置一定的重试机制,如果超过一定的次数,则将消息的状态变更为不能成功发送,并等待进行人工处理。 - 消息需要去保证幂等性。
可以看到的是,在上面的步骤中,设计到了大量的重试机制,那么消息极有可能是重复发送的。
(1)比如由于网络问题,生产者将消息发送到队列成功后,反馈却没有成功的返回过来,从而进行重新发送。
(2)比如消息消费者在处理完业务逻辑后,basicAck确认前宕机了,那么消息会被消费者再次消费。
这些都会导致业务逻辑的重复执行,因此我们必须要保证消息的幂等性。如果业务本身就是带主键的插入操作,利用主键的唯一性可以保证幂等性。或者我们可以增加防重表,或者利用redis也行,去保证业务的幂等性。
利用可靠消息实现最终一致性是一个可以支持高并发的方案,因为它不需要去锁定数据库资源,所以在对数据的实时性要求没有特别高的情况下,可以用它来支撑一定的并发。