原文地址
本文所述方法是基于理论与实践
你可曾想过多少次,在基于特定上下文环境给一个领域对象开发新的服务时,一开始用简单的CRUD架构一点问题都没有,随着时间的推移周围的生态系统持续增长和扩展,直到你开始注意到以下两点:
1、为对象执行查询时变得很复杂(需对其他服务调用多个HTTP,表之间昂贵的join等等)。
2、写操作的性能下降,因为服务的读操作多于写操作。
当然,以上提出的一些问题可以通过很多方法解决。经过深思熟虑,我能想出一些数据模式如分片策略,对数据库物化视图,数据报告卸载(数据仓库),读取和更新使用不同的数据库访问技术,可能有一些人通过使用不同的组合模式或方法来解决。
然而,本文我将把重点放在我最喜欢的CQRS模式上。
CQRS解决的是什么问题?
当使用像CRUD这样的传统架构时,使用相同的数据模型来更新和查询大型数据库,最终可能会成为负担。例如:
- 读请求可能在查询端不同的源上执行多个查询,返回具有不结构的复杂DTO映射。我们知道映射可能会变得非常复杂。
- 在写请求,模型可能实现多个复杂的业务规则来验证创建和更新操作。
- 我们可能希望以其他方式查询模型,将多个记录合并为一个,或者将更多当前在其域上不可用的信息聚集到模型中,或者通过使用一些辅助字段作为键来更改查询记录的方式。
因此,围绕模型对象的CRUD服务开始做太多的事情,随着服务增长会变得更糟。然后,CQRS这个模式就进入了我们工具箱,帮助我们解决这些可伸缩性问题。
CQRS模式
CQRS:是命令和查询责任分离的简写。其主要目标是将数据的更新和读取操作进行分离的简单思想。为了实现这个目标,将读操作和写操作分离为不同的模型,使用命令来创建和更新,用查询来读取数据。
如上图所示,你会注意到每次应用的写请求包含一个实例被创建或更新时,会向一个事件队列的topic发布事件来连接读写请求。之后,查询服务将从传入事件中读取数据,对数据进行非规范化、修改、切片和分片,以创建查询优化模型并存储它们供以后读取。
尤其是,我将在本系列文章中通过添加事件方式来使用CQRS模式。如果我们要达到读写的分离或异步操作,这种方式是很适合的。使用合适的数据库引擎提高查询性能。(比方说,一个用于写请求的SQL数据库和一个用于对物化视图进行查询操作的NoSQL,这个物化视图对查询进行了优化,以避免昂贵的join)
此外,当我们使用事件源体系结构时,事件主题将成为我们的核心数据源,因为它可以在任何时候用于对整个事件集合的重复使用并再现数据的当前状态。 因此,当系统发展时,或者当读取模型必须更改时,我们可以从一开始就异步读取队列,并从原始数据生成一组新的物化视图。实体化视图实际上是数据的持久只读缓存。
读写分离的另一个好处是,各自可以独立扩展,减少锁竞争。此外,由于大多数复杂的业务逻辑都包含在写模型中,因此通过分离模型可以使它们更加灵活并简化维护。阅读可以相对简单。
什么时候使用CQRS模式
和其他架构模式一样,CQRS有时很有用,但不是总是有用的。我们在很多场合都认识到,没有什么银弹可以解决我们所有的问题。但是,它在以下可能的条件下是有用的:
- 读性能必须与写性能分开调优,特别是当读的次数远远大于写的次数时。在这个场景中,您可以向外扩展读模型,只在几个实例上运行写模型。
- 在这种情况下,一个开发团队可以专注于编写模型中复杂领域模型,而另一个团队可以专注于读取模型和用户界面。
- 系统会扩展,并可能包含模型的多个版本,或者业务规则会定期更改。
- 与其他系统集成,特别是与事件源结合时,其中一个子系统的暂时故障不应该影响其他子系统的可用性。
- 允许读取最终一致的数据。因为这个模式的异步特性。
总结
正如我上面所说的,这并不是我们所有伸缩/查询问题的解决方案。我们在使用CQRS模式时应该非常谨慎。它有时有用,但并不总是有用。许多系统使用CQRS模式工作的很好,是因为足够简单。因此把一些易于管理的东西证明是一个非常复杂的架构将浪费时间。所以,除非有必要,否则尽量让事情简单化。请记住,从队列读取数据并进行所有数据转换来反规范化原始输入,对于时间消耗和资源使用来说可能非常昂贵。
然而,尽管很复杂,但我很容易发现这种模式非常有用,比如在面临需要以多种方式查询模型而不影响写性能的问题时。不仅如此,还值得强调这样一个事实:我们可以重用整个队列来创建原始数据的新视图,从而使查询更加简单和廉价,因为我们可以避免表连接和数据合并。
下一步
当然,该模式还有许多其他方面需要在一篇较长的文章中讨论,但我希望在这里至少强调该架构的核心原则。因此,接下来让我们将接进入本系列的第二部分,通过使用GoLang、Kafka、MongoDB和Cassandra编写一些简单的示例,来了解这个模式的实际用途以及动态组件的交互是如何进行的。下一集见!