后端系统中的可扩展读写操作方案


当您从头开始构建后端系统时,一切都会看起来很美好。API 响应速度极快(例如,100 毫秒响应时间),资源消耗看起来很稳定,最重要的是用户很高兴使用您的系统,这会让您为您的系统及其架构感到自豪。

随着时间的推移,一个潜在客户肯定会增长很多倍,这就是您系统中的“数据”。您必须很好地处理它并寻找创新的解决方案来满足可扩展性需求。在本文中,我将展示为什么以及如何将读取和写入数据视为单独的操作,以及这将如何帮助后端系统很好地扩展以满足对系统不断增长的需求。

添加索引以加快读取速度
改进数据库中读取操作的最简单解决方案是为经常查询的列引入索引。对于大多数中型系统,这就可以完成工作,并且不必过度优化其数据存储以进行读写操作。
在引擎盖下,数据库引擎创建指向数据在数据库中存储位置的指针,以便它使用这些指针来提高读取性能。

但是这反过来又减慢了写操作。在使用表索引之前,请确保您知道这种权衡。

从只读副本读取
只读副本是主数据库实例的精确副本,其中的更改近乎实时地传播。只读副本有助于卸载主实例,因为它可能面临繁重的负载,从而提高读取操作的速度。只读副本有时用于其他与应用程序无关的目的,例如分析,以便它们执行的操作不会影响主实例,用户也不会受到影响。

使用只读副本,写操作也不会受到影响,因为您没有像索引一样改变表本身的性质。但这是有代价的。设置和管理单独的只读副本将花费更多,并且需要明智地维护基础架构。

从缓存中读取或物化读取
如果必须连接和查询大量表来为用户提供读取流量,如果数据在增长,则它不可扩展,因为查询可能会变慢,并且用户必须等待更长的时间才能获得响应。构建物化读取并在主表中更新数据后立即对其进行更新,并且由于它是预先计算的,因此服务于物化读取的读取流量要快得多且可扩展。

如果主数据源或表负载很重,并且数据变化不频繁,建议构建一些缓存来为用户提供读取流量。应该注意缓存对象的失效和更新,以便它保持与真实数据的更新。

从 API 角度分离读写
同时执行读取和写入操作的 API 不能容错,也不能有效扩展。假设有一个 API(POST 请求),用户通过输入一些详细信息来验证自己。实际验证时,我们必须调用第三方系统并取回验证状态。我们将验证请求和状态存储在数据库中。

作为对此 API 的响应,我们会发回成功或失败的验证状态。如果验证时间过长,用户将不得不一直等待,有时会超时。假设同一用户尝试 10 次仍然面临失败。如果同样的情况发生在 1000 个用户上,这将变成 10000 个失败的请求,并且还会增加你的后端系统的资源消耗。

为了扩展此功能并使其更具容错性,可以将读取和写入操作分为 2 个不同的 API。与上述相同的场景,即使失败,也只能通过写入 API 为用户接收一次验证请求,用户可以通过读取 API 检查其验证状态 n 次。它不会对系统造成任何问题,因为写入和读取操作是分开的,并且没有在同一 API 中返回验证状态的合同。

使用 CQRS 分离读取和写入数据存储
为了比之前将读写操作分离到不同的 API 的方法更进一步,可以使用CQRS模式进一步根据读写操作在逻辑和物理上分离数据存储。因此缩写为 Command Query responsibility segregation。通过这样做,读取和写入数据存储可以拥有针对每个操作优化的完全不同的技术。例如,用于写入的关系数据库、用于读取的 NoSQL 数据库。

读取和写入数据存储必须同步,以确保数据在数据存储之间保持一致。采用事件驱动方法和 CQRS 模型将使您的系统具有高度可扩展性和最终一致性。

使用高级范例(如事件驱动系统、CQRS 等)来分离读取和写入操作以获得更好的可扩展性和容错性,需要不同的思维方式并从根本上重新思考我们如何构建数据密集型应用程序。最终的一致性需要被接受并且应该与我们的产品和功能的景观相匹配,它是如何直观地设计和构建的,让您的用户处于中心位置。