5.9 KiB
5.9 KiB
可扩展性设计与路线图 (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. 为什么不建议按品类「分表」
- 核心场景是全局检索:用户通常不知道商品属于哪个品类,搜索要跨所有品类。
按品类拆成多表后,一次搜索得
UNION ALL所有表,更慢、代码更复杂、排序更难统一。 - 单表足够能打:Postgres 单表配好索引,几千万行量级的检索完全可承载。 "表大"很少是真正瓶颈,"搜索方式"和"读并发"才是。
- 分表会侵蚀框架优势: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_kind做 LIST 分区(对上层透明,仍是一张逻辑表)。主要利好 维护(分区级 vacuum / 归档)与域内查询裁剪;对真正的全局搜索帮助有限。
阶段 2 — 搜索相关性/规模成为痛点:引入专用搜索引擎
触发:pg_trgm/tsvector 在相关性排序、跨字段检索、规模上吃力。
- 把检索迁到专用倒排引擎:OpenSearch / Meilisearch / Typesense,
或 Postgres 内的 ParadeDB(
pg_search,BM25)。 - Postgres 仍是唯一事实来源;搜索引擎只做索引,由 ingestion 在写库后同步。
- 这才是"搜索量大"的正解,比分表有效得多。
4. 大批量导入的建议
- 海量初始化/回填用
COPY而非逐行INSERT。 - 超大批量时可"先停建二级索引 → COPY → 重建索引",比边插边维护索引快得多。
- ETL 控制并发与批大小,避免与在线读争抢。
5. 药品档案怎么落地(回到最初的问题)
在上述设计下,加"药品"属于阶段 0 的常规扩展,不触动架构:
- 新增
drug的kind_field模板(批准文号 / 通用名 / 商品名 / 剂型 / 规格 / 生产企业 / OTC 分类 / 适应症 / 用法用量 / 不良反应 / 禁忌 / 注意事项 / 贮藏 / 有效期 等),qualified标记关键字段参与合格度评分。 - 加一棵药品品类子树,并把这些品类的
archive_kind置为drug。 - 仅当药品需要被独立筛选/排序的强结构化字段(如按批准文号精确查、按 OTC 分类
过滤)时,才考虑补一张
drug_detail明细表;否则继续走attributesJSONB。
6. 版本
- 本路线图随规模演进更新;任何落地改动需同步:迁移(SQL) + 本文档 +(涉及对外字段时)
docs/data-contract.md/docs/openapi.yaml。