5-事件驱动的分布式数据管理

本文出自Nginx官网,是微服务介绍系列文章的第五篇。原文地址:https://www.nginx.com/blog/event-driven-data-management-microservices/

1.介绍

在第一篇文章中我们比较了微服务架构应用和单体应用的差异,讨论了微服务架构的优点与缺点;第二篇文章和第三篇文章中讨论了进程间通信相关的问题;第四篇文章讨论服务发现问题;在本篇文章中我们讨论分布式数据管理相关的问题。

2.微服务下分布式数据管理面临的问题

单体应用一般有单一的关系型数据库,使用关系型数据库的主要好处是应用可以使用ACID事务,提供事务操作的原子性、一致性、隔离性和持久性等保证。应用使用关系型数据库时先开启事务,再进行多个表的增删改查操作,最后提交事务,这样就能保证数据一致性。

关系型数据库的另外一个优点是它支持功能丰富、声明式、标准化的SQL语言。使用SQL语言很容易实现多表查询,数据库管理系统会选择最优执行路径,开发人员不需要考虑如何访问数据库。

在微服务架构下,数据访问变复杂了,因为数据归每一个微服务所有,只能通过服务接口访问。通过服务接口对数据封装能确保微服务之间的松散耦合和独立进化,如果多个服务都访问同一部分数据,对数据更新时就要花费大量时间协调多个服务的更新。

服务可能使用不同类型的数据库,这让问题更复杂。现代应用要处理各式各样的数据,很多情况下,关系型数据库并不是最优选择。某些场景下,NoSQL数据库可能有更简洁的数据模型、更好的性能和伸缩性:存储和查询文本的服务使用像Elasticsearch这样的文本搜索引擎更合适;存储和处理社会关系的服务使用像Neo4j的图数据库更方便。微服务架构的应用经常混合使用SQL和NoSQL数据库,这就是所谓的多语言持久化方法。

数据存储使用分区的、多语言持久化架构有很多优点,能实现服务间松散耦合、具有更好的性能和伸缩性,但随之而来的,也带来了分布式数据管理的挑战。

第一个挑战是如何实现跨服务的事务,下面以在线商店为例具体讨论。在线商店应用中,顾客服务拥有顾客信用卡限额的信息;订单服务管理用户订单,在下订单之前必须确保订单金额不超过顾客信用卡限额。在单体应用中,订单服务简单使用ACID事务就能实现顾客信用卡限额检查、下订单。

微服务架构下,订单表和顾客表分属于不同的服务,是私有的,其他服务不能直接访问,图示如下:


订单服务不能直接访问顾客表,只能通过顾客服务的接口访问。理论上订单服务可以使用分布式事务(两阶段提交)实现,实际上,在现代应用中,分布式事务不是可选项。CAP定理要求你在可用性和ACID风格的一致性之间选择,而可用性一般都是更明智的选择;另外,很多现代技术(像NoSQL数据库)不支持两阶段提交。既然如此,在多个服务和数据库之间保持一致性又很关键,我们需要其他解决方案。

         第二个挑战是如何实现从多个服务中获取数据。假设应用需要展示顾客和最近订单,如果订单服务提供根据顾客查询订单的接口,则可以使用应用侧联合的方式实现:先从顾客服务中获取顾客,再根据顾客从订单服务中获取订单;如果订单服务只支持按照主键获取信息(假设使用NoSQL数据库且只支持根据主键查询),这就没有简单的方法能获取到数据了。

3.事件驱动架构

对很多应用而言,事件驱动架构是一种解决方案。在这种架构下,有需要关注的事情发生时(比如服务更新了业务实体),就发布一个事件;其他服务订阅该服务的事件,在监测到事件后根据业务逻辑更新自己的实体,从而引发更多事件的发布。

可以使用事件实现跨服务的事务,事务包括一系列步骤,每一步都包括更新业务实体、发布事件,从而触发下一步。下面系列图示展示了如何使用事件驱动的方法实现信用卡额度校验;在图示中,服务之间通过消息中间件交换事件。

1.订单服务创建状态为“NEW”的订单,并发布订单创建事件。


2.顾客服务消费订单创建事件,发布信用保留事件。


3.订单服务消费信用保留事件,将订单状态变更为“OPEN”。


假设:a.每个服务自动更新数据库、发布事件;b.消息中间件保证事件最少被分发一次;这就能实现跨越服务的事务。需要指出这不是ACID事务,它只能提供最终一致性这样弱的保证,这种事务模型也叫基本模型(可参见:https://queue.acm.org/detail.cfm?id=1394128)。

还可以使用事件模型构建连结多个服务数据的物化视图。维护视图的服务订阅相关事件,根据事件更新视图。比如,可以实现一个顾客订单更新服务来维护顾客订单视图,它订阅顾客服务和订单服务的事件,如下图:


当顾客订单更新服务接收到顾客服务和订单服务的事件时,它会更新物化视图的数据存储;你可能使用类似MongoDB的文档数据库存储视图数据,每个顾客一个文档。顾客订单视图查询服务基于视图数据提供顾客查询和最近订单查询。

事件驱动架构的优点:能实现跨服务的事务,提供最终一致性保证;应用可以维护物化视图;缺点:1.为了从应用级故障中恢复,需要实现补偿事务;比如,如果信用卡额度校验失败,需要主动撤销订单。2.应用可能读到不一致数据(物化视图更新前的数据)。3.订阅者需要忽略重复消息。

4.实现原子操作

在事件驱动架构中,需要保证更新数据和发布事件是原子操作。在示例中,订单服务在订单数据表中插入一行数据和发布新订单事件必须在一个事务中,如果系统在插入数据后崩溃,需要回滚插入数据的操作,不然就会造成不一致。保证原子操作的一般办法是在数据库和消息中间件之间构建分布式事务,但是,就像之前描述的,基于CAP定理,不会使用分布式事务实现。

使用本地事务发布事件

实现原子操作的一个办法是使用本地事务进行多步操作。这种方法需要创建一个事件表,它类似消息队列,用于保存所有业务实体的状态。应用更新业务表,同时更新事件表,这两步操作放在一个事务中;另外创建进程查询事件表,负责将事件发布到消息中间件,再使用本地事务将事件表中的事件置为已发布。以下图示展示了过程:


订单服务向订单表中插入一行数据,再向事件表中添加订单创建事件;事件发布进程查询事件表中所有未发布的事件,发布事件,再使用本地事务将事件状态修改为已发布。

         这种方法的好处是在不使用分布式事务的前提下,保证了业务表的更新都会发布事件,发布的事件是业务级的;缺点是需要开发人员编码发布事件,容易因为遗忘而出错,还有就是对某些事务能力和查询能力较弱的NoSQL数据库而言,不容易实现。

挖掘数据库事务日志

应用更新数据库时,会导致数据库事务日志的变化,日志挖掘进程通过读取数据库事务日志的方式,实现事件发布。下图展示了此过程:


这种办法的一个示例是开源的LinkedIn Databus项目,Databus挖掘oracle数据库的事务日志,根据日志变化发布事件。LinkedIn使用Databus保持多个衍生数据存储和核心业务数据的一致性。

另外一个示例是AWS DynamoDB中的流机制。DynamoDB流包括按时间顺序排列的过去24小时内DynamoDB表中所有数据项的变化(包括创建、更新和删除操作),应用可以从流中读取变化、发布事件。

事务日志挖掘方式的优点是将事件发布从业务逻辑中隔离出来,简化了应用开发;缺点是不同数据库甚至同一数据库的不同版本,日志格式不一致,需要针对开发,另外一个问题是将底层数据库的变化转化到高层的业务事件可能比较困难。

使用事件源

事件源机制使用完全不同的,以事件为中心的业务实体持久化方法。事件源机制不保存业务实体的当前状态,它按时间顺序保存状态变化事件,应用可以重放事件来构建最新状态。事件源机制在业务实体状态变化时创建新的事件,附加到事件列表中;因为保存事件是单一操作,它天生支持原子性。

以订单实体为例讨论事件源机制的运行。在传统的做法中,每个订单对应数据库订单表中的一行数据;在事件源机制下,订单是一组记录业务实体状态变化的事件(创建、审核、取消等),每个事件包含重建订单状态的所有数据。过程见下图:


每一个事件都持久化在事件存储中,事件存储提供添加和获取事件的接口。事件存储就像消息中间件,它提供接口支持其他服务订阅事件,负责将事件分发给订阅该事件的服务。事件存储是事件驱动微服务架构的骨架。

事件源机制的好处:它保证事件驱动架构下业务实体状态变化时,事件能可靠发布;解决了微服务架构下的数据一致性问题;因为它保存的是事件不是领域对象,不需要做对象和关系型数据库的映射;它提供了100%可靠的数据库审计,能查询任意时间点的数据库状态;它能保证组成业务逻辑的服务间松散耦合。在单体应用中使用事件源机制可以简化向微服务架构迁移的工作。

事件源机制的缺点:它不同于常见的编程模式,有一定的学习曲线;事件存储直接支持的查询只有一种:根据业务实体的主键查询,需要使用CQRS(Command Query Responsibility Segregation)支持复杂查询;应用需要操作持久化的事件数据(而不是业务实体数据)。

5.总结

在微服务架构下,每个微服务都有自己的私有数据存储,不同的微服务可能使用SQL或者NoSQL数据库,这带来了分布式数据管理的挑战。第一个挑战是如何实现跨服务的事务,第二个挑战是如何跨服务的查询。

对于大多数应用来说,解决方案是使用事件驱动架构。实现事件驱动架构需要考虑如何实现更新状态和发布事件的原子性,有几种实现方式:包括将数据库作为消息队列、事务日志挖掘和事件源机制等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章