前言:最近,在家里养伤,由于博主骑自行车不小心摔跤了,给自己造成了影响,同时也给公司造成了影响,没有按时报到。希望大家骑自行车时一定要小心,手里不要拿手机,还是那句话:道路千万条,安全第一条,行车不规范,亲人两行泪。好了,这是血的教训。今天的主题不是教如何骑自行车,哈哈哈。言归正传,利用在家养伤总结一下面试中经常问到的在微服务架构中如何解决分布式事务的问题。因为,这个问题,当时回答的不是太好,下来也查询很多资料,算是总结一下学习的心得,如果有不对的地方还请大佬们能多多指点。
一、理论
首先这个问题是出现在微服务架构、分布式环境中的,在单机系统中是不用考虑这个问题的,首先我们来看下分布式系统的CAP原则和BASE理论。
我们知道,这个三个特征最多只能满足两个,三者不可兼得。一致性:所有节点在同一时间点所有的数据都是一致的。可用性:在任何时候分布式系统总是可以成功读和写。分区容忍性:在某些节点因为网络故障时,仍然能够满足一致性和可用性的服务。
选择CA:放弃p 等同于放弃分布式系统,只存在于单机系统
选择CP:也就是选择分区容忍性和强一致性,允许在极端情况下出现暂时服务的不可用。
选择AP:允许出现数据的短时不一致,在服务注册的场景短期的数据不一致,不会对服务造成影响。因此采用AP原则的注册中心才是微服务比较合适的选择。
然后,介绍一下BASE理论,如图底部的词汇,BASE指Basically Available 基本可用,Soft-state 软状态(状态允许有短时间不同步,异步), Eventual Consistency 最终一致性,它是对CAP中一致性和可用性的权衡的结果,BASE的核心思想是即使无法做到强一致性,也可以根据系统特性,采用适当的方式达到最终一致性。基于这样的理论知识,我们只要做到最终一致性就可以解决分布式系统中的问题了。在分布式系统中,最重要的是满足业务需求,而不是追求抽象、绝对的系统特性。
如果感觉还是不太明白建议参考CAP框架作者的这篇文章:https://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html
二、场景
一图胜千言:
三、解决方案分析
解决方案:
(1)刚性事务
全局事务(标准的分布式事务)
优点:严格的ACID;
缺点:效率非常低(微服务架构下已经不太合适)
原因:1)全局事务方式下,全局事务管理器(TM)通过XA接口使用二阶段提交协议(2pc)与资源层(如数据库进行交互)。使用全局事务,数据被lock的时间跨整个事务,直到全局事务结束。
2)2pc是反可伸缩模式,在事务处理过程中,参与者需要一直持有资源直到整个分布式事务结束,这样当业务规模越来越大的情况下,2pc的局限性就越来越明显,系统可伸缩性就会变的很差。
3)与本地事务相比,XA协议的系统开销相当大,因而应当谨慎考虑是否确实需要分布式事务。而且只有支持XA协议的资源才能参与分布式事务。
(2)柔性事务
可靠消息最终一致性(异步确保型)
TCC(两阶段型、补偿型)【略】
最大努力通知(非可靠消息、定期校对)【略】
我们现在知道有这么多解决分布式事务的方案,我们能否自己去实现一个分布式事务框架呢?假如我们选择柔性事务中的,可靠消息最终一致(异步确保型)这种方案来实现就不得不涉及到消息中间件的使用了,消息中间价在分布式系统中的主要作用是,异步通讯、解耦、削峰填谷等。如下图所示:
如果按照上面的图,大家可能认为它很简单呀,一个发送消息,一个接受消息。但是在分布式系统中,需要通过网络进行通信的,就引入了数据传输的不确定性,也就是CAP理论中的P(分区容错性的问题),如下图所示:
很显然,正是因为跨网络,就会产生消息发送不一致的问题,也就是说,如果我的业务操作成功,那么有这个业务操作所产生的消息一定要成功投递出去,否则就丢失消息。那么我们该怎么解决这样的问题呢?
也就是如何保障消息发送一致性?通常我们在使用消息队列来处理业务都会遇到这样的场景:
public void CompleteOrder() { //订单处理 orderService.OrderProcess(); //财务处理(发送消息) ...... }
(1)如果业务操作成功,执行消息发送前应用故障,消息发送不去去,导致消息丢失(订单系统和财务系统数据产生不一致)
(2)如果业务操作成功,应用正常,但消息系统故障或者网络故障,也会导致消息发送不出去(订单系统和财务系统数据产生不一致)。
另外一种处理方案:
public void CompleteOrder() { //财务处理(发送消息) ...... //订单处理 orderService.OrderProcess(); }
也就是,我先发送消息再处理订单,这种做法更不可控了,消息发送出去了,但是订单处理失败了(导致订单和财务系统数据的不一致),记住在处理这种场景,一定是业务数据先入库,再发送消息的。
貌似这两种方式都不能保证业务数据的一致性,当然,我们可以借助JMS标准中的XA协议方式来保证消息发送的一致性,但是这种方式引入了XA,违背了柔性事务的初衷,会带来很多局限性:
(1)要求业务操作的资源(也就是数据库)必须支持XA协议(并不是所有的数据库都支持XA)
(2)两阶段提交协议的成本
(3)持久化成本等DTP模型的局限性(全局锁、成本高、性能低)
另外一种变通的做法如下:(这种方案只是解决的业务处理成功,消息一定能发送到消息中间件中)
问题:
(1)上面的消息发送一致性方案的正向流程是可行的,但是如果遇到异常流程该怎么处理?
(2)消息发送到消息中间件能得到保障,但是消息的准确消费又如何保障呢?
(3)有没有支持这种发送一致性流程的现成消息中间件?
先来聊聊问题(1),有哪些场景会出现异常,如下图所示:
从主动应用方(生产者)来分析:
(1)预发消息失败,消息未进行存储,业务操作未执行(可能的原因:主动方应用、网络、消息中间件、消息存储等),这种场景不会出现不一致性。
(2)预发送消息后,主动方应用没有收到返回消息存储结果,可能的状态有:消息未进行存储,业务操作未执行(不会出现不一致性);如果消息已进行存储【待确认】,业务操作未执行,会出现不一致性。
(3)收到消息存储成功的返回结果,但未执行业务操作就失败,可能的状态有:消息已进行存储【待确认】,业务操作未执行,会出现不一致性的问题。
从消息中间件的角度分析:
(1)消息中间件没有收到主动方应用的业务操作处理结果,可能的状态:消息已经存储(待确认),业务操作未执行(或者业务操作出错回滚了),会出现数据的不一致性问题;如果消息已经存储(待确认),业务操作成功,也会出现数据的不一致性。
(2)消息中间件收到业务操作结果(成功/失败),但处理消息存储中的消息状态失败,可能的状态:消息已经存储(待确认),业务操作未执行(或业务操作出错回滚了),会出现数据的不一致性问题;如果消息已经存储(待确认),业务操作成功,也会出现数据的不一致性问题。
那我们该如何处理这样的异常问题呢?如下图所示:
这样就可以处理异常的流程,有人可能会说,异常处理流程也可能发生异常呀,其实大家认真看的话,异常处理基本上都是查询,如果查询出现了异常,定时补救也是可以的。
总结:关于上面异常处理总结
(1)消息未进存储,业务操作未执行,不会出现一致性问题。
(2)消息已进行存储(待确认),业务操作未执行,会出现一致性问题,异常处理手段,确认业务操作结果,处理消息【删除消息】
(3)消息已进行存储(待确认),业务操作成功,会出现一致性问题,异常处理手段,确认业务操作结果,处理消息【更新消息状态,执行消息投递】
对于问题(3)有没有现成的消息中间件支持消息发送一致性,答案是:没有。是因为常用的MQ队列消息的处理流程无法实现消息发送一致性,因此直接使用现成的MQ中间件产品无法实现可靠消息最终一致性的分布式解决方案。
对于问题(2)消息发送到消息中间件能得到保障,但是消息的准确消费又如何保障呢?
以前的流程,如下图所示:
红框圈住的就是消息的消费流程,在这个流程中,任何一个环节都有可能出问题具体到下面这张图:
那我们如何处理,这些异常呢?处理的方法有很多,这里列出常规的做法:对于未确认的消息,采用按照规则重新投递的方式进行处理。这里就不介绍了, 当然,我们需要处理极端情况:消息重发也得有次数限制,要不然就变成了死循环,对于超过重发次数的消息,进入到死信队列,等待人工干预或者延后定期处理。同时也需要处理被动方在处理业务时要实现幂等操作。
四、总结
好了,暂时分享到这里吧,虽然在分布式系统中已经有现成的框架,比如.net core 中的 CAP框架,非常的不错,我们生产环境中已经用了很长时间,框架能帮助我们解决实际场景下的问题,但是我们也要明白框架背后的原理,明白大概的原理,再去看CAP的源码,你会恍然大悟的!哈哈哈哈。
参考资料:
龙果学院 吴水成老师 《微服务架构的分布式事务解决方案》 https://www.roncoo.com/details/7ae3d7eddc4742f78b0548aa8bd9ccdb
CAP框架作者 杨老师 https://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html
作者:郭峥
出处:http://www.cnblogs.com/runningsmallguo/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。