paint-brush
表格作为 API?幻想与现实经过@truewebber
新歷史

表格作为 API?幻想与现实

经过 Aleksei Kish9m2025/03/05
Read on Terminal Reader

太長; 讀書

作者认为,使用共享数据库表作为服务间通信的手段是一种反模式。虽然这似乎是一种快速解决方案,但它会导致版本控制问题、所有权不明确以及可扩展性和安全性困难。相反,本文提倡“契约优先”方法,其中每个服务都正式定义其接口并保留对其自身数据的所有权。这种方法可以促进更明确的责任制、更顺畅的演进和更强大的跨团队集成。
featured image - 表格作为 API?幻想与现实
Aleksei Kish HackerNoon profile picture
0-item
1-item

介绍

服务交互中的“合同”是什么?

在交互服务模块的背景下,不可避免的问题出现了:通信遵循什么规则?在 IT 产品中,“合同”代表对系统之间流动的数据及其传输方式的正式理解。这需要数据格式(JSON、Protobuf 等)、结构元素(字段、数据类型)、通信协议(REST、gRPC、消息队列)和其他规范。


合同确保开放性(每个人都知道收到和发送了什么)、可预测性(我们可以更新合同并维护版本)和可靠性(如果我们做出管理良好的更改,我们的系统就不会失败)。

为什么人们倾向于选择数据库中的表作为“合同”。

在实践中,尽管大家都在谈论微服务、“契约”和 API,但我们经常看到人们采取这样的方法:“为什么不在数据库中创建共享表,而不是构建 API?”


  • 历史或组织习惯:当所有内容始终存储在一个公司内的一个数据库系统中时,为什么要重新发明轮子呢?


  • “快速修复”心态:我们写,您读,无需设置授权规则和设计 API 规范。


  • “大数据”论点:当处理数十甚至数百 GB 的数据时,直接传输到共享表似乎更简单、更快速、更经济,但在实践中,它会产生可扩展性和性能问题以及数据所有权问题。


因此,虽然使用共享表进行数据交换看似高效且可以快速获得结果,但从长远来看,它会带来各种技术和组织挑战。然而,当团队选择共享表进行数据交换时,他们在实施过程中可能会面临许多问题。

为什么“数据库中的表”不是契约(以及为什么它是一种反模式)。

缺乏明确定义的界面

当服务通过 REST/gRPC/GraphQL 进行通信时,它们具有正式定义:OpenAPI(Swagger)、protobuf 模式或 GraphQL 模式。这些详细定义了哪些资源(端点)可用、需要哪些字段、它们的类型以及请求/响应格式。当“共享表”充当合同时,没有正式的描述:没有合同的正式描述;只有表模式(DDL)可用,而且甚至没有很好的文档记录。对表结构的任何细微修改(例如,添加或删除列、更改数据类型)都会影响读取或写入此表的其他团队。

版本控制和演进问题

API 版本控制是一种常规做法:我们可能有 v1、v2 等等,我们可以保持向后兼容性,然后逐步将客户端迁移到较新的版本。对于数据库表,我们只有 DDL 操作(例如ALTER TABLE ),这些操作与特定的数据库引擎紧密相关,需要谨慎处理迁移。


没有一个集中式系统可以向消费者发送有关架构更改的警报,要求他们更新查询。因此,可能会发生“私下”交易:有人可以在聊天中发帖说“明天,我们将把 X 列改为 Y 列”,但不能保证每个人都能及时做好准备。

所有权不明确

当 API 定义明确时,谁拥有它就一目了然:作为 API 发布者的服务。当多个团队使用同一个数据库表时,对于谁来决定结构、存储哪些字段以及如何解释这些字段,就会产生混淆。因此,该表可能成为“无人拥有的”,并且每次更改都成为一项任务:“我们需要与另一个团队核实,以防他们使用旧列!”

安全和访问控制问题

如果许多团队都有权访问数据库,那么很难跟踪谁可以读取和写入表。未经授权的服务有可能访问数据,即使这些数据不是为他们准备的。使用 API 可以更轻松地管理此类问题:您可以控制访问权限(谁可以调用哪些方法)、使用身份验证和授权以及监视谁调用了什么。使用表则要复杂得多。

对内部结构的依赖

对数据的任何内部修改(重新组织索引、对表进行分区、更改数据库)都会成为全局问题。如果表充当公共接口,则所有者无法在不危及所有外部读者和作者的情况下进行内部更改。

实践中的痛点与典型问题

协调变化

这是最痛苦的部分:如何通知另一个团队第二天架构将会改变?

  • 更新表版本的成功场景:所有者创建一个新表,其架构与旧表并行更新。旧版本仍可供当前消费者访问,所有者向他们发送一条消息,称“新结构可用;请查看文档和截止日期。请在两个版本都存在时进行迁移。”


  • 然而,在 OLAP 场景中或数据量较大的情况下,维护两个并行表并非易事。您还必须确定如何将数据从旧模式移动到新模式。这有时可能需要计划停机或非常复杂的基础设施。这个过程必然会带来风险和额外的工作。

数据完整性问题

当多个团队使用共享表来选择和更新关键数据时,它很容易变成“战场”。结果是业务逻辑最终分散在不同的服务中,并且没有对数据完整性的集中控制。很难知道为什么某个字段以特定方式存储,谁可以更新它,以及如果将其留空会发生什么。

调试和监控挑战

例如,假设表损坏:假设有坏数据或有人锁定了一些关键行。确定问题的根源通常需要询问每个具有数据库访问权限的团队,以确定哪个查询导致了问题。这通常并不明显:这意味着一个团队的查询可能锁定了数据库,而另一个团队的查询产生了可观察到的错误。

单节点故障会拖累所有人。

共享数据库是单点故障。如果它发生故障,那么许多服务都会随之发生故障。当数据库由于一项服务的查询繁重而出现性能问题时,所有服务都会遇到问题。在具有明确 API 和数据所有权的模型中,每个团队都是其服务可用性和性能的主人,因此一个组件的故障不会传播到其他组件。

提供单独的只读副本并不能解决问题。

一种常见的折衷方案是:“我们会为您提供一个只读副本,这样您就可以在不影响我们主数据库的情况下进行查询。”起初,这可能会解决一些负载问题,但是:

  • 版本控制问题仍然存在。主要问题是,当主表结构发生变化时,副本的结构也会发生变化,只是会有一些延迟。


  • 复制滞后可能导致数据状态不可预测,尤其是在大型数据集的情况下。


  • 所有权仍不明确:谁来定义格式、结构和使用规则?副本仍然是其他人数据库的“一部分”。

如何正确设计服务交互(契约优先)

明确的合同定义。

现代设计实践(例如“API 优先”或“契约优先”)始于正式的接口定义。使用 OpenAPI/Swagger、protobuf 或 GraphQL 模式。这样,人和机器都知道哪些端点可用、哪些字段是必需的以及使用哪些数据类型。

数据所有者服务

在微服务(甚至是模块化)架构中,假设每个服务完全拥有其数据。它定义结构、存储和业务逻辑,并为所有外部访问该 API 提供 API。没有人可以接触“其他人的”数据库:只有官方端点或事件。每当有变化时,这都会让生活变得更轻松,而且总是清楚谁应该负责。

实现示例

  • REST/HTTP:服务发布诸如GET /itemsPOST /items等端点,客户端使用明确定义的数据模式(DTO)发出请求。


  • gRPC/二进制协议:在 gRPC/protobuf 中,服务和消息在 .proto 文件中正式定义,并且只需对定义方法、请求和响应的 .proto 文件进行更改即可。


  • 事件驱动:数据拥有服务将事件发布到 Kafka 或 RabbitMQ 等代理,订阅者消费这些事件。这里的契约是事件格式。结构更改通过版本化主题或消息进行。

版本控制

无论哪种模型,在接口上实现版本控制都是可能的,也是必要的。例如:

  • 在 REST 中,我们有 /api/v1/… 和 /api/v2/。


  • 使用 gRPC/protobuf,有强大的向后/向前兼容机制——可以添加新的字段、消息和方法而不会破坏旧客户端,而其他字段、消息和方法则标记为弃用。


  • 在事件驱动架构中,您可以并行发布新旧事件格式,直到所有消费者迁移。

分布式责任

一个基本原则是,拥有数据的团队可以决定如何存储和管理数据,但他们不应向其他服务授予直接写入权限。其他人必须通过 API,而不是编辑外部数据。这会产生更清晰的责任分配:如果服务 A 出现故障,那么修复它是服务 A 的责任,而不是它的邻居。

服务交互示例

在单个团队内

乍一看,如果一切都在一个团队中,为什么要用 API 把事情复杂化呢?实际上,即使你将一个产品分成多个模块,共享表也会导致同样的问题。


  • 最好创建一个拥有“订单”表的“外观”或“微服务”,然后其他模块(如分析)调用这个外观/服务。


  • 这使得合同原则保持明确并简化调试。


例如,订单服务是订单表的所有者,而计费服务不直接访问该表 - 它调用订单服务的端点来获取订单详细信息或将订单标记为已付款。

两队之间

在更高层次上,当两个或多个团队负责不同的领域时,原则保持不变。例如:

  • 团队 A 负责包含每件商品信息(价格、可用性、属性)的产品目录服务。


  • B 团队负责购物车服务。


如果团队 B 直接查询属于团队 A 的“Catalog”表,则 A 的任何内部模式更改(例如,添加字段、更改结构)都可能影响团队 B。


正确的方法是使用 API:团队 A 提供GET /catalog/itemsGET /catalog/items/{id}等端点,团队 B 使用这些方法。如果 A 能够支持旧版本和新版本,他们可以发布 /v2,这让 B 有时间进行迁移。

组织方面和优势

透明沟通

有了正式合同,所有更改都是可见的:在 Swagger/OpenAPI、.proto 文件或事件文档中。任何更新都可以事先讨论、适当测试和安排,并根据需要制定向后兼容策略。

更快的开发速度

一项服务的变化对其他服务的影响较小。如果团队妥善管理新旧字段或端点,确保平稳过渡,就不必担心“破坏”其他人的服务。

访问和安全管理

API 网关、身份验证和授权(JWT、OAuth)是服务的标准配置,但使用共享表几乎不可能实现。更容易微调访问权限(谁可以调用哪些方法)、保存日志、跟踪使用情况统计数据和施加配额。这使系统更安全、更可预测。

结论

数据库中的共享表是实现细节,而不是服务之间的协议,因此不被视为合同。许多问题(复杂的版本控制、混乱的变化、不明确的所有权、安全性和性能风险)使这种方法从长远来看无法维持。


正确的方法是契约优先,即通过正式设计定义交互并遵循每个服务仍然是其数据所有者的原则。这不仅有助于减少技术债务,而且还能提高透明度、加快产品开发速度,并实现安全更改,而无需参与数据库模式的斗争。


这既是技术问题(如何设计和集成),也是组织问题(团队如何沟通和管理变更)。如果您希望产品能够不断发展,而不必处理与数据库架构相关的无休止的紧急情况,那么您应该开始考虑合同,而不是直接数据库访问。