微服务架构之事件驱动架构

数据库 来源:lintingte 485℃ 0评论

前言

为了解决传统的单体应用(Monolithic Application)在可扩展性、可靠性、适应性、高部署成本等方面的问题,许多公司(比如Amazon、eBay和NetFlix等)开始使用微服务架构(Microservice Architecture)构建自己的应用。

微服务架构( 维基百科 ):

微服务(Microservices) 是一种软件架构风格 (Software Architecture Style),它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模组化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通讯。

但是,微服务架构在带来一系列好处的同时,也带来了若干挑战。除了分布式系统固有的复杂性以外,微服务架构也深刻影响了应用和数据库之间的关系, 与传统多个服务共享一个数据库的方式不同,微服务架构每个服务都有自己的数据库 。对于开发者来说,这就为微服务中的数据管理提出了更高的要求。

微服务架构中的数据管理

在传统的单体应用中,通常使用单个的关系型数据库。这类数据库所提供的事务语义,具备 ACID 特性。

ACID:

  • Atomicity(原子性):一个事务中的操作是原子的,其中任何一步失败,系统都能够完全回到事务前的状态

  • Consistency(一致性):数据库的状态始终保持一致

  • Isolation(隔离性):多个并发执行的事务不会互相影响

  • Durability(持久性):事务处理结束后,对数据的修改是永久的

应用得益于数据库的这些特性,能够用简单的方式对数据进行修改与读取,而无需花费太多精力考虑数据一致性问题。

但是,在微服务架构下,为了在微服务之间建立松耦合的关系,通常每一个微服务都会拥有自己独立的数据库,仅仅通过对外暴露的API来进行数据交换。这种情况下,我们就要面临分布式数据管理带来的挑战。也就是说, 在实现业务逻辑时,如何保证服务之间的数据一致性 。

实时一致性

我们首先考虑在系统中实现实时一致性的情况。比如以一个银行系统为例,客户通常会有一个储蓄账户和一个理财账户。现在,考虑客户从自己的储蓄账户向理财账户转账10000元的场景。

假设现在有两张表 deposit_account 和 finance_account,分别用于存储储蓄账户和理财账户的信息,用户的ID是201。那么,在单一数据库场景下,通过数据库事务可以很容易完成这个操作:

Begin transaction update deposit_account_table set amount=amount-10000 where userId=201;
    update finance_account amount=amount+10000 where userId=1;
End transaction commit;

这样在单体应用中,由于所有数据都是保存在同一个数据库中,通过数据库提供的ACID特性,就可以轻松实现数据的实时一致性。

但是,在微服务架构中,可能的设计是存在两个服务:储蓄服务(Deposit Service)和理财服务(Finance Service),假设由储蓄服务负责处理客户的转账请求。而如下图所示,这两个服务都分别维护自己的数据,因此储蓄服务无法直接访问理财服务的数据,而只能通过API去修改客户的余额。

此时,为了满足订单服务与客户服务之间的实时一致性要求,可以采用分布式事务,比如基于 两阶段提交协议(Two-phase commit, 2PC) 的实现来做到这一点。(关于2PC,已经有大量的研究成果和成功实践经验,本文将不再做太多阐述,具体可自行参见相关文献和资料)

根据 CAP定理 ,我们追求实时一致性时,通常需要牺牲掉部分可用性。比如以上场景中,当 Finance Service 由于软硬件故障或网络问题而不可用的时候,系统将无法为用户提供内部转账服务。

此外,作为典型的同步操作,2PC也存在着比较比较严重的性能问题,并不适合高并发场景。因此,在数据一致性上我们需要寻求其他的解决方案。

最终一致性

如果我们考虑只保证系统的最终一致性,那么就可以避免使用2PC,从而提高系统可用性和性能。

仍然以以上的用户内部账户之间的转账服务为例。当用户从储蓄账户向理财账户转账时,减少储蓄账户的金额与增加理财账户的金额这两个动作,可以无需在一个事务里面完成,而是分成两步:

  1. 储蓄服务减去储蓄账户中的金额,并生成一个凭证(消息)发送给理财服务;

  2. 理财服务收到凭证后,在理财账户中增加相应的金额。

我们会发现以上过程在第1步完成之后,第2步完成之前,储蓄账户与理财账户之间实际上是存在短时间的数据不一致的。但是,只要最终第2步能够完成,系统的数据就仍然能够保持一致性,这就是我们所说的最终一致性。

在最终一致性这个前提下,即使理财服务在某段时间内不可用,系统仍然能够能为用户提供内部转账服务,从而提高了系统的可用性。

而这样一种基于最终一致性的解决方案,就是本文将要介绍的 事件驱动的架构(Event-driven Architecture) 。

事件驱动的架构

所谓事件驱动的架构,也就是 使用事件来实现跨多个服务的业务逻辑 。

在这一架构里,当有重要事件发生时,比如更新业务数据,某个微服务会发布事件,其它微服务则订阅这些事件;当某一微服务接收到事件就可以更新自己的业务数据,同时发布新的事件触发下一步更新。而事件的发布与订阅,则依赖于一个可靠的消息代理(Message Broker)。

以上文的场景为例,在事件驱动的架构中,从储蓄账户转账到理财账户的过程如下:

  1. 储蓄服务将用户的储蓄账户中的金额减少10000,并发布“向理财账户转账”事件;

  2. 理财服务获取“转账到理财账户”事件, 更新理财账户,将理财账户的金额增加10000,并发布“理财账户转入”事件;

  3. 储蓄服务获取“理财账户转入”事件,结束本次转账交易。

在这里需要考虑的一个问题就是转账失败处理。比如以上第2步如果因为“理财账户被冻结无法转入资金”之类的原因失败了,理财服务就应该发布“理财账户转入失败”事件,储蓄服务获取到该事件后,需要对储蓄账户进行回滚,将减少的金额重新增加回去。