Files
goods/docs/scalability.md
T
rosemariejebbjtxbfp 3b62f61288
CI / Go (api) (pull_request) Successful in 59s
CI / Python (ingestion) (pull_request) Failing after 20s
CI / Migrations (postgres) (pull_request) Successful in 34s
docs: 新增可扩展性设计与路线图 (scalability roadmap)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-24 05:29:08 +00:00

5.9 KiB
Raw Blame History

可扩展性设计与路线图 (Scalability Roadmap)

本文档回答一个长期问题:随着商品越来越多、品类越来越杂(食品 / 电子 3C / 药品 / ……),检索与新增会不会压垮数据库?要不要按品类「分表」?

结论先行:现阶段不要按品类手动分表。 现有「单表 + JSONB + archive_kind 框架」 的设计方向是对的。扩展应当靠 分区(非分表) + 读副本 + 缓存 + 专用搜索引擎, 按数据量分阶段推进,避免提前过度设计。


1. 现状盘点

1.1 数据模型

  • 所有商品落在一张 product;非食品的领域字段存 product.attributes(JSONB)。
  • 食品有独立明细表 food_detail(配料 / 营养 / 过敏原等结构化字段)。
  • 0010_archive_kinds 引入 archive_kind 框架:每个品类带一个 archive_kind (food / electronics / generic),kind_field 表按 kind 定义字段模板, 驱动后台动态表单与合格度评分。
  • 加新品类(如药品)不需要新建表:只要新增一组 kind_field 行 + 一棵品类子树; 仅当某品类有大量需被独立筛选/排序的结构化字段时,才考虑像 food_detail 那样补一张 明细表。

1.2 已有索引(检索性能的基础)

对象 索引 用途
product.gtin 唯一索引 条码精确查
product.name trigram GIN (pg_trgm) 名称模糊/相似匹配
product.search_tsv 全文 GIN (tsvector) 全文检索
product.attributes JSONB GIN 属性过滤
brand.name trigram GIN 品牌模糊匹配
product.country_of_origin btree 产地精确/前缀过滤
category / brand / updated_at btree 关联与增量

1.3 检索方式

  • 有关键词时:name ILIKE word_similarity ≥ 阈值(~0.42) 条码匹配, 排序按 相似度 × (0.5 + quality_score)
  • 无关键词时:按 quality_score 排序。
  • 翻页:LIMIT / OFFSET

1.4 写入特征

  • 写库只有 Python ingestion 一条路径(批量 ETL),不是高并发 OLTP。
  • 插入瓶颈极低;search_tsv 由触发器逐行重算,正常增量下开销可忽略。

2. 为什么不建议按品类「分表」

  1. 核心场景是全局检索:用户通常不知道商品属于哪个品类,搜索要跨所有品类。 按品类拆成多表后,一次搜索得 UNION ALL 所有表,更慢、代码更复杂、排序更难统一
  2. 单表足够能打:Postgres 单表配好索引,几千万行量级的检索完全可承载。 "表大"很少是真正瓶颈,"搜索方式"和"读并发"才是。
  3. 分表会侵蚀框架优势:archive_kind 框架的价值就是"加品类零建表";手动分表等于 把这套通用能力又拆碎。

区分两个概念:分表(sharding,应用层拆多张表)分区(Postgres 原生 declarative partitioning,对上层透明的一张逻辑表)。后者在超大规模时才有意义, 见第 3 阶段。


3. 分阶段路线图(按数据量触发,不提前做)

阶段 0 — 现在 ~ 数百万条:维持现状 + 低成本优化

触发:当前规模。改动小、收益稳,建议尽早做:

  • 深翻页改 keyset 分页:OFFSET 越翻越慢(需扫描并丢弃前 N 行);改用 WHERE (score, id) < (:last_score, :last_id) 形式的游标分页。
  • 部分索引:绝大多数查询限定 status='active',可建 CREATE INDEX ... WHERE status='active' 缩小索引、提速。
  • 常用筛选复合索引:如 (category_id, quality_score DESC)(archive_kind, quality_score DESC) 配合域内列表。
  • Redis 缓存(已在技术栈内):缓存热门搜索结果与商品详情,挡住重复读。

阶段 1 — 千万级以上:读扩展 + 调优

触发:单库读 QPS 升高、P99 变慢。

  • 只读副本(read replica):本服务是只读公益 API,天然适合一主多从, 把检索/详情读流量分到副本,主库只承接 ingestion 写入。
  • 索引与查询调优:按慢查询日志补/删索引,EXPLAIN ANALYZE 校核计划。
  • (可选)Postgres 原生分区:若多数检索"限定单一域"(只搜药品 / 只搜食品), 可按 archive_kindLIST 分区(对上层透明,仍是一张逻辑表)。主要利好 维护(分区级 vacuum / 归档)与域内查询裁剪;对真正的全局搜索帮助有限。

阶段 2 — 搜索相关性/规模成为痛点:引入专用搜索引擎

触发:pg_trgm/tsvector 在相关性排序、跨字段检索、规模上吃力。

  • 检索迁到专用倒排引擎:OpenSearch / Meilisearch / Typesense, 或 Postgres 内的 ParadeDB(pg_search,BM25)
  • Postgres 仍是唯一事实来源;搜索引擎只做索引,由 ingestion 在写库后同步。
  • 这才是"搜索量大"的正解,比分表有效得多

4. 大批量导入的建议

  • 海量初始化/回填用 COPY 而非逐行 INSERT
  • 超大批量时可"先停建二级索引 → COPY → 重建索引",比边插边维护索引快得多。
  • ETL 控制并发与批大小,避免与在线读争抢。

5. 药品档案怎么落地(回到最初的问题)

在上述设计下,加"药品"属于阶段 0 的常规扩展,不触动架构:

  1. 新增 drugkind_field 模板(批准文号 / 通用名 / 商品名 / 剂型 / 规格 / 生产企业 / OTC 分类 / 适应症 / 用法用量 / 不良反应 / 禁忌 / 注意事项 / 贮藏 / 有效期 等),qualified 标记关键字段参与合格度评分。
  2. 加一棵药品品类子树,并把这些品类的 archive_kind 置为 drug
  3. 仅当药品需要被独立筛选/排序的强结构化字段(如按批准文号精确查、按 OTC 分类 过滤)时,才考虑补一张 drug_detail 明细表;否则继续走 attributes JSONB。

6. 版本

  • 本路线图随规模演进更新;任何落地改动需同步:迁移(SQL) + 本文档 +(涉及对外字段时) docs/data-contract.md / docs/openapi.yaml