海量数据下如何实现分布式事务

迈莫coding

共 5306字,需浏览 11分钟

 ·

2021-09-26 15:37


点击上方蓝色“迈莫coding”,选择“设为星标”

 


今天和大家聊一聊分布式系统中分布式事务,无论是初级程序员,还是中高级程序员,在面试过程中分布式事务问题都是必备的。但不同阶段的要求是不同的。一般被分布式事务问题,脑海中直接蹦出解决方案:2pc、3pc、MQ等等。但是这几种方案他的落地可行性考虑过吗?什么样的场景下使用什么样的方案,思考过吗?

 

接下来,我会以一个案例作为切入点进行阐述,来去分析每种方案的可行性。


案列背景

 

在互联网分布式场景中,原先单机系统会被拆分成多个子系统,进行独立部署。这样就会导致想要完成一次写入操作,你就需要协同多个子系统,这就带来分布式事务问题(分布式事务:一次大操作被拆分成多个小操作,而多个小操作分布在不同的服务器上,分布式事务就需要保证多个小操作要么同时成功,要么同时失败)。问题来了,怎样设计才能保证数据的一致性问题?这也是今天这篇问题所阐述的中心。

 

案例以商城为例,用户订购某个商品后,会涉及到订单系统、库存系统、优惠券系统(为了方便,就拿这三个系统阐述)。只有这三个系统执行成功后,才能表示该用户下单订购成功。否则下单失败。

 

 

可以从图中看出,用户购买某个商品,需要库存系统、订单系统、优惠券系统这三个系统的协调,才能保证用户下单成功。而由于每个系统可能会进行独立部署,这就导致会出现分布式事务问题,如何保证数据的一致性,并且在保证数据的一致性前提下,如何降低时延性,也是考察一个人的技术功底。

 


案例分析

 

这个案例一看就是典型的分布式事务问题,解决方案也有很多,比如2PC、3PC、基于MQ等等解决方案。但实际业务场景不太会用到2PC、3PC,互联网基本上是通过MQ来解决分布式事务问题的。

 

所以在面试回答上可以先提出解决分布式事务问题的几种解决方案,比如2PC、3PC、基于MQ消息队列。然后分析每个方案优缺点,最终选择出自己的方案。这样回答,会让面试官认为应聘者有自己独立的思考见解,不仅理论知识扎实,也会结合业务场景选出合适的解决方案。这样非常加分的。

 

但做到上述回答模式,需要对理论知识有更深次的理解,对每个解决方案都有自己独立的思考,分析出不同解决方案的优缺点。所以接下来,我会和大家一起来分析在分布式事务场景中三种解决方案。


案例解答

1
基于2pc提出的解决方案

2pc是分布式事务教父级协议,也是数据库领域解决分布式事务最典型的协议。它的处理过程分为准备阶段和提交阶段,每个阶段由协调者(Coordinator)和参与者(Participant)共同完成。

 

  • 准备阶段:在准备阶段,协调者将通知事务参与者准备提交事务,然后进入表决过程。在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功)或取消(本地事务执行故障),在第一阶段,参与节点并没有进行Commit操作。

 

  • 提交阶段:在提交阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消这个事务。必须当且仅当所有的参与者同意提交,协调者才会通知各个参与者提交事务,否则协调者将通知各个参与者取消事务。

 

参与者在接收到协调者发来的消息后将执行对应的操作,也就是本地 Commit 或者 Rollback。

 

那么它的执行过程到底是怎么样的?以本案例为基准,假设订单数据,商品数据和优惠券数据分别保存在数据库 D1,数据库 D2 和数据库 D3 上。

 

  • 准备阶段:协调者会通知参与者(订单系统、库存系统、优惠券系统)准备提交事务,相当于在准备阶段,参与者已经执行完commit阶段之前所有操作,当所有的参与者返回YES时,那么协调者进入提交阶段,此时第一阶段结束。

 

 

  • 提交阶段:当收到所有数据库实例的 Yes 后,协调者会发出提交指令。每个数据库接受指令进行本地操作,正式提交更新数据,然后向协调者返回 Ack 消息,事务结束。

 

 

上述描述的就是2PC实现的基本原理。但是仔细想想,又会发现2PC会导致数据不一致、死锁、性能低下等问题。这也是我们在技术选型中需要考虑的。

 

2PC缺点

 

  • 死锁:在执行过程中,一旦协调者出现故障,会导致数据库阻塞,尤其在提交阶段,如果发生故障,数据还处于资源锁定状态,将无法完成后续的事务提交操作。

 

  • 性能低下:2PC阶段中,数据库会对资源进行锁定,如果此时其他事务相对被锁定的数据进行操作时,发现数据处于锁定状态,那么他们只能进行阻塞等待,导致出现高延迟和性能低下问题。

 

  • 数据不一致:在提交阶段,协调者向参与者发送commit请求,此时由于网络异常,导致部分参与者没有收到commit请求,无法进行提交事务,导致数据不一致。

 

上述就是使用2PC解决分布式事务的基本原理,及其使用2PC会导致的问题。

2
基于3pc提出的解决方案

为了解决2PC同步阻塞问题,三阶段提交在协调者和参与者中都引入超时机制,并且把二阶段提交中准备阶段划分为两步:先询问再锁定资源,最后真正提交。

 

三阶段中的 Three Phase 分别为 CanCommitPreCommitCommit 阶段。

 

 

  • Can-Commi阶段:3PC 的 CanCommit 阶段其实和 2PC 的准备阶段很像。协调者向参与者发送 Can-Commit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No 响应。

 

  • Pre-Commit阶段:协调者根据参与者的反应情况来决定是否可以继续事务的 PreCommit 操作。根据响应情况,有以下两种可能。

 

1. 假如协调者从所有的参与者获得的反馈都是 Yes 响应,那么就会进行事务的预执行:发送预提交请求,协调者向参与者发送 PreCommit 请求,并进入 Prepared 阶段;

 

2. 假如有任何一个参与者向协调者发送了 No 响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就中断事务:发送中断请求,协调者向所有参与者发送 abort 请求;参与者收到来自协调者的 abort 请求之后,执行事务的中断。

 

  • Commit阶段:该阶段才是事务真正的提交,根据响应情况,分为两种可能。

    • 协调者收到了所有参与者的ACK响应,那么协调者向参与者发送Commit操作,所有参与者收到命令后,真正提交事务,执行成功后,释放锁定资源,返回协调者ACK响应,完成事务。

    • 假如协调者未收到部分参与者的ACK响应,可能是参与者返回的不是ACK确认机制,也有可能是超时,中断事务。

    • 协调者收到所有参与者的ACK响应后,由于网络原因,部分参与者未收到commit命令,那么参与者超市之后也会执行事务提交操作。

 

上述就是3PC的实现原理。在2PC基础之上进行改进。

 

3pc与2pc的改进之处


  • 引入超时机制:在2PC阶段中,只有协调者拥有超时机制,而在3PC阶段中,协调者和参与者都引入超时机制,保证参与者即使遇到协调者宕机情形下,也可以超时释放锁定资源,反之造成死锁。

 

  • 添加预提交阶段:在 2PC 的准备阶段和提交阶段之间,插入一个准备阶段,使 3PC 拥有 CanCommit、PreCommit、Commit 三个阶段,PreCommit 是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。

3
基于MQ提出的解决方案 

基于MQ可靠消息队列是目前互联网最常用的方式,在应对海量并发数据的场景下,可以起到业务解耦和流量削峰的效果。使用MQ解决分布式事务问题,其核心采用数据最终一致性思想。

 

还是以商城案例为例,用户下单后,订单系统调用优惠券系统时,将扣减的优惠券发送到消息队列中,优惠券系统拉取数据进行对应业务操作,然后保证优惠券系统可以正常消费成功就可以了,因为消息队列会进行持久化,即使MQ服务器发生宕机,当重启时消息也不会丢失。

 

 

采用MQ可靠性队列不仅保证数据的一致性,解决业务流程同步流程阻塞问题,还起到了业务解耦和流量削峰的效果,这也是互联网最常用的方式。目前市场上技术选型很多,比如RabbitMQ、RocketMQ等等。

 

但使用MQ可靠性队列作为解决分布式事务的方案,那么光知道这些还不够,你需要把MQ如何保证数据不丢失,MQ的提交机制等等了解清楚后,才能够合理运用。遇到意外发生,也能在萌发阶段扼杀掉,不至于背锅。😂😂。

 

MQ自动应答机制导致数据丢失

 

MQ消息中间价默认情况下采用自动提交机制,也就是说优惠券系统消费数据后,消息中间件会删除这个持久化的消息。

 

如果采用自动提交机制,就会出现数据丢失的可能性。比如优惠券系统消费时,由于不可控原因导致其消费失败,那这个时候消息中间价中就没有这条数据了,导致数据丢失。如果采用手动提交机制,优惠券系统消费成功后,才会返回ACK确认机制,可以保证数据一定消费成功,消息中间件才会删除这条持久化数据。

 

MQ数据挤压导致数据丢失

 

分布式系统是基于网络进行通信,而在网络通信的过程中,上下游可能因为各种原因而导致消息丢失。比如优惠券系统由于流量过大而触发限流,不能保证事件消息能够被及时地消费,这个消息就会被消息队列不断地重试,最后可能由于超过了最大重试次数而被丢弃到死信队列中。

 

但实际上,你需要人工干预处理移入死信队列的消息,于是在这种场景下,事件消息大概率会被丢弃。出现这个原因主要是订单系统无法感知优惠券系统的执行状况,优惠券系统无法通知订单系统导致的。

 

那么基于这条思路,我们这样干:在订单系统中维护一个消息状态表,每条数据发送成功到消息中间件后,在消息状态表中更新数据未已发送状态;当优惠券系统消费完成后,更新状态表未已完成状态;最后定时任务不断轮训消息状态表,获取未完成状态的消息,将其再次投递到消息中间件中进行消费。这样就可以保证数据一定不丢失。

 

 

基于这样的设计方案,会有一个问题,会导致消息重复问题。比如此时优惠券系统执行成功前一秒,定时任务扫描到该数据处于未完成状态,将其再次投递到消息中间件中,而此时优惠券系统更新消息状态表。那么这种情况会导致数据消费,所以需要优惠券这边做处理。

 

接下来我们就来谈谈优惠券系统如何保证数据不重复消费问题。

 

MQ保证数据幂等性

 

上述通过消息状态表解决消息丢失问题,但随之引来的是消息重复消费问题。为此,优惠券系统作为消费者,在消费时需要考虑幂等性,在消息处理之前首先查看数据是否已消费,若未消费才进行消费,否则不消费。


总结

 

最后对这篇文章进行总结。今天讲述了解决分布式事务的几种方案,以及各个方式的优缺点。

 

  • 基于 MQ 的可靠消息投递的考核点是可落地性,所以你在回答时要抓住“双向确认”的核心原则,只要能实现生产端和消费端的双向确认,这个方案就是可落地了,又因为基于 MQ 来实现,所以天生具有业务解耦合流量削峰的优势。

 

  • 基于 2PC/3PC 的实现方案很少有实际的场景,但你还是要掌握它的实现原理和存在的问题,因为面试不同于实际工作,有些问题的回答是为了告诉面试官:我有这个能力。尽管它在实际工作中并不适用。

 

最后,有一点需要你注意,在实际工作中,并不是所有的业务对事务一致性的要求都那么高。因为更高的要求意味着更多的成本,这也是很多架构复杂度来源之一,所以你要尽可能地站在业务实际场景的立足点来回答分布式事务问题。

 

参考资料

1.https://kaiwu.lagou.com/course/courseInfo.htm?courseId=592#/detail/pc?id=6054

2.https://kaiwu.lagou.com/course/courseInfo.htm?courseId=69#/detail/pc?id=1906


分割线




往期推荐


一文读懂Redis的哨兵机制

http响应状态码该怎么用?

elasticsearch集群扩容和容灾

实习四个多月,聊一聊自己的感受

go语言十分钟入门教程

HTTP1.0和HTTP1.1和HTTP2.0的区别


文章也会持续更新,可以微信搜索「 IT界学习笔记 」第一时间阅读。每天分享优质文章、大厂经验、大厂面经,助力面试,是每个程序员值得关注的平台。




  1. 你点的每个赞,我都认真当成了喜欢


浏览 15
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报