在 Azure Cosmos DB for PostgreSQL 中为多租户 SaaS 应用建模

适用对象: Azure Cosmos DB for PostgreSQL(由 PostgreSQL 的 Citus 数据库扩展提供支持)

租户 ID 作为分片键

租户 ID 是工作负荷根目录处的列,或数据模型中层次结构的顶部。 例如,在此 SaaS 电子商务架构中,它是应用商店 ID:

Diagram of tables, with the store_id column highlighted.

此数据模型对于 Shopify 等业务来说是典型的。 它托管多个在线商店的网站,其中每个商店都与其自己的数据交互。

  • 此数据模型包含一组表:商店、产品、订单、行项和国家/地区。
  • 存储表位于层次结构的顶部。 产品、订单和行项都与商店相关联,因此在层次结构中较低。
  • 国家/地区表与各个商店无关,它是跨商店的。

在此示例中,store_id 位于层次结构顶部,是租户的标识符。 这是正确的分片键。 选择 store_id 作为分片键可以跨单个工作器上的单个存储的所有表并置数据。

按商店分配表具有优势:

  • 提供 SQL 覆盖,例如外键、JOIN。 单个租户的事务在存在每个租户的单个工作器节点上进行本地化。
  • 实现单位数毫秒性能。 单个租户的查询将路由到单个节点,而不是并行化,这有助于优化网络跃点,并且仍可缩放计算/内存。
  • 它会缩放。 随着租户数量的增长,可以添加节点并将租户重新平衡到新节点,甚至将大型租户隔离到自己的节点。 租户隔离允许提供专用资源。

Diagram of tables colocated to the same nodes.

多租户应用的最佳数据模型

在此示例中,我们应该按存储 ID 分发特定于存储的表,并创建 countries 引用表。

Diagram of tables with store_id more universally highlighted.

请注意,特定于租户的表具有租户 ID 并且是分布式的。 在我们的示例中,商店、产品和 line_items 是分布式的。 其余表是引用表。 在我们的示例中,国家/地区表是引用表。

-- Distribute large tables by the tenant ID

SELECT create_distributed_table('stores', 'store_id');
SELECT create_distributed_table('products', 'store_id', colocate_with => 'stores');
-- etc for the rest of the tenant tables...

-- Then, make "countries" a reference table, with a synchronized copy of the
-- table maintained on every worker node

SELECT create_reference_table('countries');

大型表都应具有租户 ID。

  • 如果要将现有多租户应用迁移到 Azure Cosmos DB for PostgreSQL,则可能需要稍微非规范化,并将租户 ID 列添加到大型表中(如果缺少该列),然后回填该列的缺失值。
  • 对于 Azure Cosmos DB for PostgreSQL 上的新应用,请确保租户 ID 存在于所有特定于租户的表中。

确保以复合键的形式将租户 ID 包含在分布式表的主要、唯一和外键约束上。 例如,如果表具有主键 id,则将其转换为复合键 (tenant_id,id)。 无需更改引用表的键。

有关最佳性能的查询注意事项

对租户 ID 进行筛选的分布式查询在多租户应用中运行效率最高。 确保查询始终限定为单个租户。

SELECT *
  FROM orders
 WHERE order_id = 123
   AND store_id = 42;  -- ← tenant ID filter

即使原始筛选器条件明确标识所需的行,也有必要添加租户 ID 筛选器。 租户 ID 筛选器虽然看似多余,但会告知 Azure Cosmos DB for PostgreSQL 如何将查询路由到单个工作器节点。

同样,在联接两个分布式表时,请确保这两个表的范围都限定为单个租户。 可以通过确保联接条件包括租户 ID 来完成范围限定。

SELECT sum(l.quantity)
  FROM line_items l
 INNER JOIN products p
    ON l.product_id = p.product_id
   AND l.store_id = p.store_id   -- ← tenant ID in join
 WHERE p.name='Awesome Wool Pants'
   AND l.store_id='8c69aa0d-3f13-4440-86ca-443566c1fc75';
       -- ↑ tenant ID filter

有多个常用应用程序框架的帮助程序库,便于在查询中包含租户 ID。 以下是说明:

后续步骤

现在,我们已探讨完可缩放应用的数据建模。 下一步是使用所选的编程语言连接和查询数据库。