现象
互联网的世界与十几年前相比,已经大不相同。以往的单体服务就可以支撑起大多数的用户需求。然而随着手机等电子产品的普及,用户想要的服务已经是越来越复杂,各种需求相互关联。而这也给软件开发带来了更多的挑战。为了应付随时会变化的代码世界,现有的开发趋势都在逐渐的化整为零。其中最具代表性的就是微服务的流行。
在软件设计时,我们经常会说高内聚,低耦合
。这其实是对功能职责的确定。而在微服务的设计里,就是要将这些职责明确后拆分出去,再联合起来提供完整的系统功能。这其实跟一家企业的运转是一样的,企业需要根据自身的情况去组建各个部门,让专业的人干专门的事,以完成共同的目标。
问题
在关于微服务的设计上,已经有很多成熟的指导方案。比如基于领域模型的,基于事件驱动的。然而,当各个服务各自为战后,在数据的一致性上,却未能有完善的解决方案。或是受性能效率限制,或是实现流程复杂。总之,我们需要采取很多额外的手段去解决分布式服务的数据一致性问题。
首先,何为数据一致性?个人理解,它可以是业务上的一致性,即数据库里经常提及的完整性约束。就像银行的转账流程,其结果导向是一方的账户余额会减少,另一方的增多。这是我们在一开始就定义好的数据流程转换,必须要正确;另一种常见的数据一致性是系统内部为了解决某些瓶颈而不得不考虑面对的问题,比如主从复制、数据分片、集群选举等,关于这一块我们后续有空再研究。先来看看分布式服务里经常需要保证的业务一致性。
在软件开发行业里,本身就有对业务一致性的解决方案,即事务。关于事务的使用到现在都不会过时,只不过需要在同一个数据源(比如同一个数据库里),别人才能给你保证。然而,在以拆分为核心思想的微服务架构下,数据的变更是会在不同地方产生的,怎么去协调这些变更,以完成定义好的业务结果,是很困难的。因为缺乏一个统一的协调管理者来介入,而一旦引入这个概念,又会挑战微服务的核心思想:去中心化,各个模块有可能会重新耦合在一起。
这就是困难点所在了,我们希望各个服务能独立运行,但服务所产生的行为数据又要能配合协调,甚至互相依赖。这就有点纠缠不清的感觉了。
分析
尽管分布式服务的数据一致性很复杂,但作为软件开发的主人,我们总得去厘清这里面的逻辑关系,确定边界,然后按统一的指导原则去设计。在这方面,已经有一个标准的分布式一致处理模型,它是由国际开发标准组织 Open Group 定制的,其核心就像前面提及到的,有一个全局的协调者:事务管理器,外加事务的参与者:资源处理器以及其他辅助组件。我们经常看到的 2PC ,两阶段提交就是基于此实现的。
两阶段提交
关于两阶段提交,它的设计是在数据层次上的一个统一协调者,它对于服务提供方来讲侵入性较少,其管理的目标是将所有参与者涉及到的数据进行统一的提交与回滚。
我们知道,在以前的本地事务里,当一系列的业务操作执行完后,就可以进行事务的提交/回滚这个确认动作了。而在两阶段提交里,这个确认动作要被延迟。之所以要延迟,是因为需要事务管理器这个协调者去查看其他参与者是否也将自己的业务流程执行完,只有当所有参与者都反馈执行完,才能进行最后的确认动作。当然,这个确认动作也是会通知到所有参与者的。
实际上,事务管理器作为全局事务的管理者,它在一开始就介入整个流程了。具体过程如下:
【1】预备阶段(Prepare phase): 事务管理器向所有事务参与者发送事务执行请求,当参与者接受到该消息后,进行本地事务的执行,记录事务日志,但并不提交事务。在执行完后会将执行结果反馈给协调者,等到后续通知。 【2】**提交阶段(Commit phase):**事务管理器根据所有参与者的执行反馈结果决定此次的操作是提交或回滚。若参与者执行失败或者超时,则会通知所有参与者进行回滚;否则,通知提交。
可以看到,我们将原本的事务流程拆分成了多个阶段,再由事务管理器去统一协调这些阶段处理。这个处理方式看起来简单,但里面要考虑的因素有很多,例如:
- 阻塞:对于参与者来讲,由于本地事务的处理结果尚未确定,所以必须要阻塞等待协调者的后续执行指令。
- 单点问题:事务管理器这个协调者很重要,一旦发送故障,那么将无法进行后续的决策,即事务参与者将会无限的阻塞,直到协调者重新上线。
除此之外,两阶段提交在性能效率上也需要衡量考虑。此时的全局事务完成时间已经不再是简单的 1+1 的计算方式了,而是参与者与协调者的协作完成时间。
TCC
两阶段提交是属于强一致性的解决方案,它在资源管理器上的实现通常是由各个参与数据库来提供统一操作:准备
、提交
、回滚
。而这一套标准操作也可以由业务来实现,以提供更细的业务粒度以及更好的并发能力,相当于服务间接的参与了全局事务的协调流程,这即所谓的 TCC:Try-Confirm-Cancel 分布式事务。
TCC 分布式事务模型也将整体流程划分了两个阶段,在第一个阶段里,会由事务管理器这个统一协调者去进行所有参与者的业务检查和资源预留,在此阶段并不会真正的执行预期业务动作,只是先 check 前置条件,占有资源。在接下来的第二阶段里,会根据各个参与者的反馈结果,去决定是进行业务的确认操作还是取消操作。
在 TCC 的设计里,会认为只要所有参与者都 Try 成功,那么接下来的 Confirm 或 Cancel 操作是肯定能成功,如果失败,那么将需要不断重新或者人工介入。因此,TCC 还得在业务上有幂等保证。
TCC 相当于让业务自定义了准备
、提交
、回滚
操作,这样可以让开发者决定资源的占有时机,降低锁冲突,提高处理能力。就是对业务的侵入很大,原有的流程需要强行定义出 Try-Confirm-Cancel 的业务操作。
消息最终一致
在实际的开发过程中,我们会发现有一些简单的分布式事务处理场景,比如签到后积分增加这种。它们可能仅仅是本地事务处理完,然后通知另外个服务进行资源更新以完成整条链路需求。对于这种简单的分布式事务处理,用户对其不一致的容忍度比较高,不要求立马生效,只要结果正确。针对这种即时性要求不高的需求,我们可以使用下面这种最终一致的方案:状态控制+消息发送+定时检查。
- 状态控制:对于本地的业务需求要有个状态记录,用于探知当前的处理阶段,以便重试或辅助后面的定时检查。
- 消息发送:当涉及其他服务的资源更新操作时,通过消息进行解耦,驱动下一个事务流程。
- 定时检查:不再有协调者去统筹全局事务,各个参与者只负责自己的事务完成情况,定时检验是否有异常状态或结果,触发对应处理流程。如报警通知、人工兜底。
上面的这个最终一致模型,适用于简单流程的分布式服务,或许跟事务
想要的效果都搭不上边,更多的是靠消息通知,事后处理这种简单手段来保证业务的最终一致。如果业务链比较复杂,那就会定义出各种各样的消息类型,这种反而会让整个系统难以理解,也不易维护。
总结
此次,我们研究了数据层次的分布式事务;业务层次的分布式事务;以及最终一致的伪分布式事务。对于这些模型的实现,市面上已经有些成熟的框架了,比如 Seata,ByteTCC 等。大家可以站在巨人的肩膀上,更深入的了解其实现流程,验证自己的所思所想。