Chris Richardson微服务翻译:微服务之事件驱动的数据管理

Chris Richardson 微服务系列翻译全7篇链接:

  • 微服务介绍
  • 构建微服务之使用API网关
  • 构建微服务之微服务架构的进程通讯
  • 微服务架构中的服务发现
  • 微服务之事件驱动的数据管理(本文)
  • 微服务部署
  • 重构单体应用为微服务

原文链接:Event-Driven Data Management for Microservices


微服务与分布式数据管理问题

单体应用一般只有一个关系型数据库,这样的好处是可以实现 ACID 保证:

  • 原子性(Atomicity):原子粒度的更改
  • 一致性(Consistency)数据库的状态始终保持一致
  • 隔离性(Isolation):并发的事务表现的像是串行执行,事务之间不会互相影响
  • 持久性(Durable):一旦事务提交就不会撤销

因此,应用可以简单的开始事务,更改(增删改)多行数据,然后提交事务。

使用关系型数据库另一好处是支持 SQL。SQL 是一种丰富的、声明式的标准查询语言,用户能简易的关联查询多个表中的数据,然后RDBMS 查询调度器会执行最优的查询方式,用户不必关系底层的细节。所有的数据在一个数据库中也方便查询。

然而微服务架构中数据访问变的复杂,因为每个微服务都拥有独立的数据库,仅能通过 API 来访问。数据封装保证了微服务的松耦合,各个服务可以独立其他服务演进。而如果多个服务访问同样的数据,架构更新会更耗费时间,也需要更多的服务协调。

不同服务可能使用不同类型的数据库,现代应用存储和处理各种各样的数据,关系数据库并不总是最好的选择。一些场景下,一种特殊的 NoSQL 能提供更方便的数据模型、更好的性能和可扩展性。例如:使用 ElasticSearch 这样的搜索引擎来进行文本的存储和查询;使用 Neo4j 这样的图谱数据库来存储社交图谱数据。因此,微服务应用会混合使用 SQL 和 NoSQL 数据库,即多态型数据持久方案。这也带来了一些挑战:

1)如何跨多个服务实现事务,维护数据的一致性。我们以 B2B 商店为例:客户服务维护用户信用额度等相关的信息,订单服务管理订单并确保新订单没有超过用户的信用额度。单体应用中,订单服务可以使用 ACID 事务来核对用户信用额度并新建订单。而在微服务架构中, order 和 customer 表是各个服务私有的:

订单服务无法直接访问 customer 表,只能通过客户服务的 API。订单服务可能使用分布式事务,也被称为两阶段提交(2PC),然而 2PC 在现代应用通常不是很好的选择。CAP 定理需要用户在可用性和 ACID 的一致性中二选一,通常可用性是更好的选择。此外,很多NoSQL 并不支持 2PC,维护服务和数据库中数据的一致性是很重要的,因此我们可以选择另一种方案。

2)另一个挑战是如何检索多个服务中的数据,例如应用需要显示一位客户和他最近的订单,如果订单服务提供了用户订单的查询 API,那么可以在应用端获取该数据,应用端通过客户服务检索客户,再通过订单服务检索该客户的订单。假设订单服务只支持通过主键来查询订单,此时就没有合适的方法来检索所需数据了。

事件驱动的架构

对于许多应用,解决方案就是事件驱动的架构:服务在业务发生时(例如更新一条记录信息)会发布一个事件,其他微服务订阅该事件,当某一微服务接收到事件就会更新自己的业务记录,然后其他更多的事件会被发布。下图展示了如何使用事件驱动的方式在创建订单时检查可用信用,微服务间通过 MQ 来交换事件:

1)订单服务创建状态为 NEW 的订单,然后发布『订单创建』的事件

2)客户服务获取『订单创建』事件,为此订单保留信用,发布『信用保留』事件

3)订单服务获取『信用保留』事件,将订单状态修改为 OPEN

更为复杂的场景会涉及更多的步骤,比如核对用户信用时预留库存。

基于(a)每个服务原子性的更新数据库并发布事件;(b)MQ 确保事件至少交付一次,那么就可以实现跨服务的业务逻辑了。这并非 ACID 事务,他提供更弱一点的最终一致性。这种事务模型可称为 BASE model

也可以使用事件维护关联多个微服务的物化视图。维护此视图的服务订阅相关事件并更新视图,例如:用户订单视图通过订阅订单事件和用户事件来进行更新:

当用户订单服务收到用户服务或订单服务的事件时,会更新视图。可以使用类似 MongoDB 的文档数据库为每个用户存储一份用户订单的文档。

事件驱动架构的优点:

  1. 他使得事务能跨多个服务并提供最终一致性;
  2. 使得应用可以维护物化视图。

不足之处:

  1. 编程模型比 ACID 事务更加复杂,为了从应用级别的错误中恢复,需要完成补偿事务,例如:信用检查不成功则必须取消订单;
  2. 临时事务会导致不一致的数据。另外应用从物化视图中读取的数据未能及时更新,也会产生不一致的问题;
  3. 必须检测并忽略重复事件

实现原子化

事件驱动架构还存在一个问题:以原子粒度更新 DB 与发布事件。例如:订单服务在订单表中 insert 一行记录,然后发布『订单创建』的事件,这两个操作需要是原子性的,否则,更新 DB 后,发布事件前服务崩溃了,系统将存在不一致。确保原子性的方法是使用 分布式事务 调用 DB 和 MQ。然而由于 CAP 理论,我们是想避免这么做。

使用本地事务发布事件

应用发布事件并保证原子性的方法之一就是:多步骤本地事务方法。技巧是 DB 中有一张 EVENT 表(模拟消息队列),存储业务数据的状态。首先启动一个本地数据库事务,更新业务数据记录并往 EVENT 表中插入一条数据,最后提交事务。一个单独的线程会轮询 EVENT 表,将查询结果往 MQ 中发送事件消息,然后使用本地事务标注事件状态为已发布。如下图所示:

订单服务首先往 ORDER 表中 insert 一行记录,然后在 EVENT 表插入类型为 Order Created 的事件(状态为 NEW )。事件发布线程或进程轮询 EVENT 表中未发布的事件,发布事件然后更新 EVENT 表事件状态为已发布。

这种方法的优点:

  1. 保证每次更新都有对应的事件发布,不使用两阶段提交(2PC);
  2. 应用发布了业务级别的事件,消除推断事件类型的麻烦。

不足:

  1. 易出错,因为要求开发者必须记得更新后去发布事件;
  2. 当使用 NoSQL 时,因为 NoSQL 的事务和查询能力有限,实现起来较麻烦。

挖掘数据库事务日志

另一种不需要两阶段提交就能实现原子性的方法是:分析数据库事务日志或提交日志来发布事件。应用更新 DB时,DB的事务日志会记录这些变更,事务日志挖掘线程或进程读取这些日志,并将事件发布到 MQ,如下图所示:

范例之一就是开源的 LinkedIn Databus 项目,Databus 挖掘 Oracle等数据库的事务日志并发布相应的事件,LinkedIn 使用 Databus 来保持各种派生数据存储和记录系统的一致。

另一范例就是 streams mechanism in AWS DynamoDB,AWS DynamoDB 流包括 DynamoDB 表在过去 24 小时内的时序变化,包括新建、更新和删除操作。应用能读取这些变更,将其作为事件发布。

事务日志挖掘的优点:

  1. 能保证无需使用两阶段提交就能对每个更新发布事件;
  2. 简化应用,将事件发布与主业务逻辑分离。

不足:

  1. 每个 DB 或 同一 DB 的不同版本的事务日志格式不同;
  2. 很难从低级别事务日志的更新记录中反推高级别的业务事件。

使用事件源

事件源通过采用一种截然不同的、以事件为中心的方法来保存业务实体,而不需要 2PC 来实现原子性。这种方法存储一系列状态变动的事件,而不是实体的当前状态。应用通过重放事件来构建实体的当前状态,每当业务实体的状态改变,就往事件列表中添加新的事件。由于保存事件是唯一操作,本质上就是原子性的。

以订单为例:传统方案中,每个订单为 ORDER 表中的一行记录。使用事件源时,订单服务存储导致订单状态变化的事件,包括创建、批准、配送、取消。每个事件由充足的信息来重新构建订单:

事件被存储 DB 中,可使用 API 添加或查找实体的事件。事件存储类似上文提及的 MQ,提供 API 让其他服务订阅事件,将事件传达至感兴趣的订阅者。事件存储是事件驱动的微服务架构的支柱。

事件源的优点:

  1. 解决了事件驱动的微服务架构的关键问题,能够可靠的发布事件;
  2. 解决了数据一致性问题,由于存储事件而不是领域对象,也避免了面向对象到关系数据库的不匹配问题;
  3. 为实体提供了100%可靠的审计日志,使得获取任何时间点的实体状态成为可能;
  4. 业务逻辑与事件交互的业务实体是松耦合的,这使得单体服务迁移到微服务更容易。

不足:

  1. 采用了陌生的编程风格,学习曲线陡峭;
  2. 事件数据库只支持通过主键查询业务实体,必须使用 CQRS(Command Query Responsibility Segregation)来完成查询,因此应用程序必须采用最终一致性。

总结

微服务架构中,每个微服务都有自己的数据存储,不同的微服务可能使用不同的 SQL 和 NoSQL 数据库。这些数据库架构有很多优势,也带来了分布式数据管理的挑战。第一个挑战就是如何实现跨服务的业务事务,并保证一致性;第二个挑战就是如何从多个服务中查询数据。

对于许多应用,解决方案就是使用事件驱动的架构。事件驱动的架构带来的挑战是如何原子化地更新状态和发布事件。有几种方案可以考虑,包括把数据库用作消息队列、事务日志挖掘和事件源。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏分布式系统进阶

KafkaBridge - Kafka Client SDK 开源啦~~~

KafkaBridge 封装了对Kafka集群的读写操作,接口极少,简单易用,稳定可靠,支持c++/c、php、python、golang等多种语言,并特别针对...

1031
来自专栏程序你好

.Net桌面系统架构设计

1411
来自专栏Java架构沉思录

缓存在高并发场景下的常见问题

当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象。这就比较依赖缓存的过期和更新策...

1524
来自专栏zhangdd.com

生产内网ssh登陆变慢问题原因及解决办法

最近发现内网一些服务器ssh连接变慢,原来都是秒开的现在基本上要等10几秒才能返回登陆界面,因为是在内网基本上排除网络连接问题

1341
来自专栏java一日一条

Java EE7和Maven工程入门(1)

在日常工作中,我经常需要解决许多简单的或者是复杂的Maven/Java EE工程结构的问题。为了找到解决办法,我经常要拿项目的结构做实验,在不同应用服务器上对部...

571
来自专栏FreeBuf

看我如何利用开发人员所犯的小错误来盗取各种tokens

实际上,在日常的开发过程中,开发人员很有可能会犯各种各样貌似“无伤大雅”的小错误,单独一个这样的小错误可能并不能搞什么事情,但如果将这些错误串起来形成一个漏洞链...

2755
来自专栏JavaEdge

Tomcat架构解析之1 架构简介1 核心架构模块说明2 分层建模3 作用域

3095
来自专栏存储

从银行转账失败到分布式事务:总结与思考

作者:xybaby 正文 思考这个问题的初衷,是有一次给朋友转账,结果我的钱被扣了,朋友没收到钱。而我之前一直认为银行转账一定是由事务保证强一致性的,于是学习、...

3336
来自专栏CSDN技术头条

【问底】夏俊:深入网站服务端技术(一)——网站并发的问题

本文来自拥有十年IT从业经验、擅长网站架构设计、Web前端技术以及Java企业级开发的夏俊,此文也是《关于大型网站技术演进的思考》系列文章的最新出炉内容,首发于...

2148
来自专栏ThoughtWorks

大型项目程序配置管理演化之路|TW洞见

今日洞见 文章作者、图片来自ThoughtWorks:窦衍森。封面图片来自网络。 本文所有内容,包括文字、图片和音视频资料,版权均属ThoughtWorks公司...

3436

扫码关注云+社区

领取腾讯云代金券