版权声明:本文为博主原创文章,转载须注明做者与来源。html
为了解决传统的单体应用(Monolithic Application)在可扩展性、可靠性、适应性、高部署成本等方面的问题,许多公司(好比Amazon、eBay和NetFlix等)开始使用微服务架构(Microservice Architecture)构建本身的应用。nginx
微服务架构(维基百科):
微服务 (Microservices) 是一种软件架构风格 (Software Architecture Style),它是以专一于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模组化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通信。程序员
可是,微服务架构在带来一系列好处的同时,也带来了若干挑战。除了分布式系统固有的复杂性之外,微服务架构也深入影响了应用和数据库之间的关系,与传统多个服务共享一个数据库的方式不一样,微服务架构每一个服务都有本身的数据库。对于开发者来讲,这就为微服务中的数据管理提出了更高的要求。sql
在传统的单体应用中,一般使用单个的关系型数据库。这类数据库所提供的事务语义,具有ACID特性。数据库
ACID:
- Atomicity(原子性):一个事务中的操做是原子的,其中任何一步失败,系统都可以彻底回到事务前的状态
- Consistency(一致性):数据库的状态始终保持一致
- Isolation(隔离性):多个并发执行的事务不会互相影响
- Durability(持久性):事务处理结束后,对数据的修改是永久的markdown
应用得益于数据库的这些特性,可以用简单的方式对数据进行修改与读取,而无需花费太多精力考虑数据一致性问题。网络
可是,在微服务架构下,为了在微服务之间创建松耦合的关系,一般每个微服务都会拥有本身独立的数据库,仅仅经过对外暴露的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,从而提升系统可用性和性能。
仍然以以上的用户内部帐户之间的转帐服务为例。当用户从储蓄帐户向理财帐户转帐时,减小储蓄帐户的金额与增长理财帐户的金额这两个动做,能够无需在一个事务里面完成,而是分红两步:
0. 储蓄服务减去储蓄帐户中的金额,并生成一个凭证(消息)发送给理财服务;
0. 理财服务收到凭证后,在理财帐户中增长相应的金额。
咱们会发现以上过程在第1步完成以后,第2步完成以前,储蓄帐户与理财帐户之间其实是存在短期的数据不一致的。可是,只要最终第2步可以完成,系统的数据就仍然可以保持一致性,这就是咱们所说的最终一致性。
在最终一致性这个前提下,即便理财服务在某段时间内不可用,系统仍然可以能为用户提供内部转帐服务,从而提升了系统的可用性。
而这样一种基于最终一致性的解决方案,就是本文将要介绍的事件驱动的架构(Event-driven Architecture)。
所谓事件驱动的架构,也就是使用事件来实现跨多个服务的业务逻辑。
在这一架构里,当有重要事件发生时,好比更新业务数据,某个微服务会发布事件,其它微服务则订阅这些事件;当某一微服务接收到事件就能够更新本身的业务数据,同时发布新的事件触发下一步更新。而事件的发布与订阅,则依赖于一个可靠的消息代理(Message Broker)。
以上文的场景为例,在事件驱动的架构中,从储蓄帐户转帐到理财帐户的过程以下:
0. 储蓄服务将用户的储蓄帐户中的金额减小10000,并发布“向理财帐户转帐”事件;
0. 理财服务获取“转帐到理财帐户”事件, 更新理财帐户,将理财帐户的金额增长10000,并发布“理财帐户转入”事件;
0. 储蓄服务获取“理财帐户转入”事件,结束本次转帐交易。
在这里须要考虑的一个问题,就是转帐失败处理。好比以上第2步若是由于“理财帐户被冻结没法转入资金”之类的缘由失败了,理财服务就应该发布“理财帐户转入失败”事件,储蓄服务获取到该事件后,须要对储蓄帐户进行回滚,将减小的金额从新增长回去。
以上的过程与传统的数据管理基于ACID模型不同的是,它是基于BASE模型的。
BASE:
- Basically Available(基本可用):系统在出现不可预知的故障的时候,容许损失部分可用性,但不等于系统不可用
- Soft State(软状态):容许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的总体可用性
- Eventually Consistent(最终一致性):系统保证最终数据可以达到一致
在事件驱动的架构中,跨服务完成业务逻辑的一个关键点是每一个服务自动更新数据库和发布事件,也就是要以原子粒度更新数据库和发布事件。例如,储蓄服务必须在对储蓄帐户表进行更新,而后发布“向理财帐户转帐”事件,这两个操做须要原子化实现。若是服务在更新数据库以后、发布事件以前崩溃,系统会变得不一致。
保证数据更新与事件发布原子化的方法,有如下几种:
- 使用本地事务发布事件
- 挖掘数据库事务日志
- 使用事件源
一个实现原子化的方法是使用本地事务来更新业务实体和事件列表,由一个独立进程来发布事件。具体来讲,就是在存储业务实体状态的数据库中,使用一个事件表来充当消息队列。应用启动一个(本地)数据库事务,更新业务实体的状态,在事件表中插入一个事件,并提交该事务。一个独立的消息发布线程或进程查询该事件表,将事件发布到消息代理,并标注该事件为已发布。下图展现了这一设计。
储蓄服务更新储蓄帐户的余额,而后在事件表中插入“转帐到理财帐户”的事件。事件发布线程或进程在事件表中查询未发布的事件并发布,而后更新事件表,将该事件标记为已发布。
这种方法的优势是:
- 使用本地事务,保证了数据被更新时事件必定可以被发布
- 实现简单,只须要系统具有本地事务的能力便可实现
这种方法的一个缺点是,数据更新操做与所要发布的事件之间的对应关系,是由应用的开发者实现的,所以有很大可能出错。
实现原子化的另外一种方式是由线程或者进程经过挖掘数据库事务或提交日志来发布事件。应用更新数据库,数据库的事务日志会记录这些变动。事务日志挖掘线程或进程读取这些日志,并把事件发布到消息代理。
好比一个B2C的电商网站,就能够经过挖掘订单数据的更新日志,来进行事件发布。以下图所示:
这一方法的范例是开源的 LinkedIn Databus 项目。Databus 挖掘 Oracle 事务日志并发布与之对应的事件,LinkedIn 则使用 Databus 维持各类来源的数据存储与记录系统一致。
另外一个范例则是 AWS DynamoDB 采用的流机制。AWS DynamoDB 是一个可管理的 NoSQL 数据库,其中每一个 DynamoDB 流包括 DynamoDB 表在过去 24 小时以内的时序变化,包括建立、更新和删除操做。应用可以读取这些变动,将其做为事件发布。
这种方法的优势是:
- 要发布的事件直接来源于数据库的事务日志,所以不会出错
- 应用无需关注事件的发布,简化了应用开发者的工做
可是这种方法也有一些缺点:
- 事务日志的格式与所使用的数据库相关,所以事件挖掘 的实现会因为数据库的种类或版本的变化而随之须要修改
- 因为是直接从数据库的更新记录生成事件,所以可能会没法逆向推断出业务逻辑,所以并不适合于全部场景(好比前文所述的转帐场景)
事件源采用一种大相径庭的、以事件为中心的方法来保存业务实体——不一样于存储实体的当前状态,应用存储的是状态改变的事件序列。每当业务实体的状态改变,新事件就被附加到事件列表,而且应用能够经过事件回放来重构实体的当前状态。鉴于保存事件是一个单一的操做,所以本质上也是原子化的。
要了解事件源如何运行,能够以储蓄服务为例。在传统的方法中,每次转帐交易都会更新储蓄帐户表的记录。而使用事件源的时候,储蓄服务以状态更改事件的方式存储用户的储蓄帐户,每一个事件都包含足够的数据去重建储蓄帐户状态。
事件长期保存在事件仓库(Event Store),使用 API 添加和检索实体的事件。同时,事件仓库起到相似上文说起的消息代理的做用,经过 API 让服务订阅事件,将全部事件传达到全部感兴趣的订阅者。因此,事件仓库能够认为是数据库与消息代理的综合体,是事件源方法的支柱。
事件源方法有以下的优势:
- 事件即状态,发布事件就是在更新状态,所以自然具备原子性,而且不会出错
- 因为存储的是事件,而不是域对象,所以避免了对象关系抗阻不匹配的问题(object‑relational impedance mismatch problem)
- 因为存储了全部的业务状态更新事件,所以能够经过事件回放推断出任一时间点的业务实体状态
事件源方法也有如下这些缺点:
- 要实现一个可靠和高性能的事件仓库并非一件容易的事情
- 应用代码须要根据事件仓库的 API 进行重写
- 事件仓库只直接支持经过主键查询业务实体,所以对于复杂视图的查询比较困难(能够经过CQRS方法解决,具体参见下文)
在事件源方法中,再也不直接存储任何业务实体的状态,而是代之以状态变动事件。在进行复杂视图的查询时,若是还按照与命令操做一样的方式,将会遇到一些困难。好比要发起以下的一个同时涉及储蓄帐户和理财帐户的查询操做:
SELECT * FROM DEPOSIT_ACCOUNT deposit, FINANCE_ACCOUNT finance WHERE deposit.user_id = finance.user_id AND finance.state = 'active' AND deposit.amount > 100000 AND finance.amount > 5000
在非事件源的方式下,能够很容易的从储蓄帐户表和理财帐户表查询到相应数据。可是在事件源方式下,事件仓库中存储的是一系列事件,而且只能经过主键(好比 deposit_account.id 或 finance_account.id)去查询相应的业务实体,此时要处理相似 deposit.amount > 100000 这样的查询条件以及条件组合时,是很是复杂和低效的。
为了解决这一问题,能够采用CQRS方法,将命令与查询分离。命令操做仍然经过各服务的 API 以更新事件列表的方式进行,而查询操做则经过一个统一的视图查询服务(View Query Service)完成。
根据存储在事件仓库中的事件集合,能够计算获得每一个业务实体的状态,这些状态以物化视图(Materialized View)的方式存储在一个数据库中。当有新的事件产生时,也一样会自动更新视图。这样,视图查询服务就能够像查询普通的数据库数据同样实现各类查询场景。具体的设计可参考下图所示:
在微服务架构中,每一个微服务都有其私有数据存储,不一样的微服务可能使用不一样的数据库。这种架构带来便利的同时,也给分布式数据管理带来挑战,其中最大的挑战就是在实现跨服务的业务逻辑时,如何保持服务之间的数据一致性。
对于许多应用,解决方案就是使用事件驱动的架构。事件驱动的架构带来的挑战是如何原子化地更新状态和发布事件。有几个方法能够作到这一点,包括把数据库用做消息队列、事务日志挖掘和事件源。