主题 / 后端与数据
17 数据库与存储
关系库的建模与命名、表结构迁移原则、事务与并发控制(隔离级别、乐观锁、行锁领取、租约)、outbox 表与幂等表、连接池与查询性能、备份与恢复演练、对象存储和向量存储怎么做。
一页结论
- 新项目默认 PostgreSQL。 客户已有 MySQL 或指定国产库就沿用;「兼容 PostgreSQL / MySQL」的库要单独跑方言测试,不假设等价。
- 约束放进数据库。 主键、非空、唯一、外键、检查约束在库里表达;时间点用带时区类型,金额不用浮点。
- 表结构只走版本化迁移。 先加后删(扩展-收缩),让新旧两版应用能同时跑;回滚靠向前修复,不靠 down 脚本。迁移在同款同版本的库上、用真实规模的数据实测。
- DDL 也会锁表。 先设锁等待超时;大表建索引用并发方式;加约束先不校验存量、再单独校验;回填分批、可续跑。
- 事务短而明确。 事务边界在业务用例层划,事务里不调外部服务;用更高隔离级别时要能整体重试序列化失败。
- 并发写有保护。 乐观锁检查影响行数;多个 worker 领取任务用 SKIP LOCKED 加租约和 fencing token;能用现成队列库就别手写(见 18)。
- outbox 与业务同事务。 投递至少一次,消费者按事件 ID 幂等;幂等表的结果和业务变更一起提交。
- 主从不是备份。 需要时间点恢复就用基础备份加连续 WAL 归档;只有恢复演练成功过的备份才算数。
- 存储先用简单的。 向量先用 pgvector,按压测结果再换专用库;对象存储只依赖 S3 的常用子集,新项目不选已归档的 MinIO 社区版。
规则
表、列、约束、索引用小写加下划线命名;可变的业务编号不当技术主键,另建唯一约束
对外暴露或跨系统生成的 ID 用 UUIDv7,单库内部的高写入表可用自增 bigint;任何 ID 都不当作访问凭证
为什么、来源
- 数据库没有内置 UUIDv7 生成函数时,在应用里用经过测试的库生成。
- 不想暴露创建时间的场景(如对象存储 key)用随机值。
- ULID 只在已有 ULID 生态时沿用,不作默认。
表示某个时刻的列用带时区的时间类型(PostgreSQL 用 timestamptz);需要本地业务时间的,另存 IANA 时区名
为什么、来源
「每天早上 9 点」这类本地时间规则存本地时间加时区名,不要提前换算成 UTC。
金额和要求精确的数值用 numeric/decimal 或约定单位的整数(如「分」),不用浮点类型
非空、唯一、外键、检查约束在数据库里表达;要去掉外键等约束,写明理由并登记
为什么、来源
- 状态值少且稳定时用 CHECK 约束或查找表;PostgreSQL 原生枚举删除或重排值很麻烦。
- JSONB 适合稀疏、变化快的扩展字段;经常用来筛选或关联的稳定属性独立成列。写明 JSON 字段的结构由谁负责、怎么演进。
先定删除语义:默认真删除;确有恢复或留存需要才用软删除,并用部分唯一索引处理唯一约束
为什么、来源
软删除表的唯一约束只约束未删除的行,例如:
CREATE UNIQUE INDEX ... ON documents (tenant_id, slug) WHERE deleted_at IS NULL。
删主表一行不等于整条链路都删了。法规上的删除义务见 27。
多个租户共用一张表时,唯一约束、外键和常用索引都包含租户列
为什么、来源
租户隔离模型(共享表、独立 schema、独立库)怎么选见 19。
使用 PostgreSQL 行级安全(RLS)时,应用连接用的角色不得是表所有者、超级用户或带 BYPASSRLS 属性,并对表开启 FORCE ROW LEVEL SECURITY
为什么、来源
- RLS 是第二道防线,不替代应用层授权。
- 租户上下文用事务级设置(如
set_config(..., true)),不用会话级设置,避免连接复用时残留(见 DB-032)。
分区只在有明确的生命周期、查询或数据量需求时使用;用分区就要自动预建未来分区并有清理流程
为什么、来源
删除或分离旧分区前,先确认已归档或已备份。
生产库的表结构只通过版本化迁移变更;迁移作为单独的发布步骤用专用角色执行,不在应用启动时自动同步或每个实例都跑
为什么、来源
用哪个迁移工具按语言选,见 10–15。ORM 自动生成的迁移必须人工审查生成的 SQL。
不兼容的表结构变更按扩展-收缩分步做:先新增、再回填和切换、最后删除旧结构;每一步都要让当前版本和上一版本的应用能同时运行
为什么、来源
典型步骤:新增可空列或新表 → 分批回填 → 校验(空值、计数、抽样比对)→ 应用切换到新结构 → 至少过一个发布周期后删除旧结构。 删除旧结构的迁移要单独评审。
回滚以向前修复为主;删除数据的迁移标明不可逆,不靠 down 脚本声称可以恢复
迁移在与生产同款、同大版本的数据库上验证:既从空库跑,也在上一版本的脱敏数据快照上跑
PostgreSQL 迁移按安全写法:DDL 前设 lock_timeout;大表建索引用 CREATE INDEX CONCURRENTLY 且不放在事务里,完成后检查索引有效;加约束先 NOT VALID 再单独 VALIDATE
为什么、来源
- 等不到锁时让迁移快速失败(错误码 55P03)再重试,比把业务堵住好。
CREATE INDEX CONCURRENTLY失败会留下无效索引,要删掉重建。- 加 NOT NULL:先加
CHECK (col IS NOT NULL) NOT VALID,再VALIDATE CONSTRAINT,确认后再SET NOT NULL。
大表变更的 PR 写明锁级别、是否重写全表、预计耗时和执行窗口;lint 管不到的风险由人审查
数据回填分批、幂等、可暂停续跑,不和 DDL 放在同一个事务里
为什么、来源
按主键游标分批,每批短事务;记录游标位置、处理量和失败量;设语句超时并限速。
MySQL 迁移不套用 PostgreSQL 的事务 DDL 假设;在线变更显式指定 ALGORITHM 和 LOCK,并在同版本实例上验证
为什么、来源
原生在线 DDL 不满足时再考虑 gh-ost 或 pt-online-schema-change,并先审查外键、触发器和复制的限制。
「兼容 PostgreSQL」或「兼容 MySQL」的数据库(如 openGauss、金仓、TiDB、OceanBase)单独跑方言测试,不假设与原版等价
为什么、来源
每个迁移文件写明适用的方言;没在目标库上测过的 SQL 不复制过去用。
事务边界在业务用例层划定;事务内不调用大模型、邮件、远程 HTTP 等外部服务
为什么、来源
- 需要外部副作用的,先在事务里写 outbox 或任务记录,提交后再执行(见 DB-027)。
- PostgreSQL 可以设
idle_in_transaction_session_timeout兜底。 - 各语言的事务写法(开启、回滚、检查提交错误)见 10–15。
按用例显式选隔离级别;默认的读已提交级别下「先读后写」有竞态的,用行锁、乐观锁或更高隔离级别保护
为什么、来源
- 单行更新:用
SELECT ... FOR UPDATE或乐观锁(DB-023)。 - 跨多行的不变量:考虑可串行化级别,并按 DB-022 重试。
使用可重复读或可串行化级别时,序列化失败(40001)和死锁(40P01)要整体重试整个事务,有次数上限和退避;唯一冲突(23505)按业务处理,不盲目重试
为什么、来源
- 只重试可以安全重复的整个数据库用例;事务里不能有已经发生的外部副作用。
- 唯一冲突通常意味着「已存在」,转成业务错误或走幂等逻辑。
乐观锁用版本列:更新时带上期望版本并递增,检查影响行数;冲突时返回业务错误,不自动覆盖
为什么、来源
UPDATE t SET ..., version = version + 1 WHERE id = $1 AND version = $2,影响 0 行时再区分「不存在」还是「版本冲突」。
编辑类冲突通常应提示用户重新加载,只有允许丢弃旧值的场景才自动重试。冲突在接口上怎么表达见 16。
SKIP LOCKED 只用于多个消费者领取队列式表里的任务,不用于需要一致视图的普通查询
为什么、来源
需要重试、优先级、定时、监控的,优先用现成的数据库队列库,不手写(见 18)。
多个 worker 领取任务时用租约:用数据库时钟记录到期时间,到期可被回收;每次领取递增一个 fencing token,完成、续约和写结果时都校验它
为什么、来源
- 领取:
FOR UPDATE SKIP LOCKED选出待办或租约已过期的行,更新状态、持有者、到期时间,并把 token 加 1。 - 写入:
WHERE id = $1 AND lease_token = $2,影响 0 行就视为租约已丢,停止处理。 - 用数据库的
now()而不是各 worker 的本地时钟;租约时长要明显长于心跳间隔。 - 外部副作用仍要靠业务幂等。分布式锁服务见 18。
业务写入和待发事件需要原子时,outbox 记录与业务变更在同一个事务里提交
为什么、来源
原则见 ARC-015。能用数据库变更捕获(CDC)的,也可以代替轮询 outbox 表。
outbox 中继按至少一次投递设计:带领取租约、重试次数上限、失败转人工处理、已发送记录定期清理;消费者按事件 ID 幂等
为什么、来源
- 最少字段:事件 ID、聚合类型与 ID、事件类型、载荷、发生时间、状态、尝试次数、下次尝试时间、发送时间。
- 同一聚合需要顺序时,显式加序号,不靠 UUIDv7 或提交时间推断。
- 中继到消息组件、消费者框架见 18;事件格式和版本见 16。
幂等表以「租户 + 操作 + 幂等键」做唯一约束并保存请求指纹;处理结果与业务变更在同一个事务里提交
为什么、来源
- 用
INSERT ... ON CONFLICT抢占;不支持该语法的兼容库,用插入加捕获唯一冲突再查询,并先测过其并发语义。 - 请求指纹按规范化后的内容计算(字段顺序不影响结果)。
- 记录过期就允许重放;资金类操作要用长期有效的业务操作 ID,不能只靠短期键。
- 状态码和响应语义(不同请求 422、处理中 409)以 16 为准。
连接预算:各服务「连接池上限 × 实例数」加上管理和迁移连接,总和不超过数据库允许的连接数
为什么、来源
每个进程只建一个连接池(各语言写法见 10–15)。连接数参数的具体值按库的规格定,不给统一数字。
启用 PgBouncer 这类事务级连接池前,先测会话状态、预编译语句、临时表、LISTEN/NOTIFY 和 RLS 租户上下文
为什么、来源
租户上下文和会话变量只用事务级设置;预编译语句的支持取决于连接池配置和驱动,要实测。
新增关键查询或改索引时,附上在有代表性数据量上的执行计划;每个索引对应具体查询,不给每列都建索引
EXPLAIN (ANALYZE, BUFFERS) 结果;定期用 pg_stat_statements 找慢查询,查未使用的索引为什么、来源
- 先测量再优化:查询写法、N+1、返回列、索引和统计信息都排查过,再考虑加缓存或读副本。
- 大数据集分页用基于游标的 keyset 方式并配合索引,不对外提供无上限的 OFFSET;分页接口约定见 16。
- 在生产上跑 ANALYZE 会真的执行语句,注意开销和脱敏。
有读副本时,为每类查询写明一致性要求;读自己刚写的数据、授权判断、事务内的读一律走主库
为什么、来源
生产数据有书面的恢复目标(RPO/RTO)、保留期、负责人和恢复手册;备份范围覆盖数据库、对象存储、配置和密钥恢复材料
为什么、来源
RPO 是最多能丢多少时间的数据,RTO 是多久内要恢复。数值按业务定,不给统一值。
主从复制和存储快照不是备份;需要时间点恢复的,用基础备份加连续 WAL 归档(MySQL 用基础备份加 binlog),不能只靠 pg_dump
为什么、来源
备份加密,密钥与备份分开保管;至少一份放在另一个故障域且生产账号不能删改;备份失败、WAL 归档落后要告警
为什么、来源
告警和值班流程见 26,密钥管理见 25。
定期在隔离环境里做真实恢复演练,校验数据完整、应用可用,并记录实际恢复时间和数据损失
跟上数据库当前的小版本,主版本在社区支持期内;升级数据库或备份工具前读发布说明,执行其中要求的额外步骤
为什么、来源
规则不写具体版本号;版本基线单独记账,按官方支持页更新。
应用通过一层适配只使用 S3 的常用子集,并在选定的存储产品上跑兼容测试;数据库元数据和对象之间用状态字段加定时对账
为什么、来源
元数据表记录状态、大小、校验和、检测出的类型、所属租户;未完成的分片上传要有清理规则。上传安全检查和预签名链接的要求见 25。
对象 key 由服务端生成,不可猜测,带租户前缀;不用用户原始文件名或个人信息做 key
向量存储先用 pgvector;按自己的数据和过滤条件压测召回率、延迟和维护成本,不达标再换专用向量库
为什么、来源
- 近似索引是先查索引再过滤,过滤条件严时返回条数会不足;pgvector 提供迭代扫描、部分索引和分区来缓解,要实测。
- 多个租户共用一个近似索引会互相影响召回和速度,可按租户分区或分表,但不要无限增加分区。
- 不写「多少条以上必须换专用库」这类阈值。切块、检索和重排策略见 24。
没有符合条件的规则。
选型
| 类别 | 默认 | 备选(何时用) | 不推荐 |
|---|---|---|---|
| 关系数据库 | PostgreSQL,使用社区支持期内的主版本和该主版本的最新小版本 · PostgreSQL License;MySQL 社区版为 GPLv2,另有商业许可,口径见 27 新项目 | MySQL(LTS 发布线)(客户指定、团队已有 MySQL 运维和备份体系,或已有大量存量) 客户已验收的国产库(openGauss、金仓、达梦、OceanBase、TiDB 等)(信创或客户指定;先拿到产品、版本和测试环境并通过方言测试(DB-019)) | 已停止支持的主版本:不再有安全修复 未正式发布的主版本:不用于生产 |
| PostgreSQL 迁移静态检查 | Squawk · MIT 或 Apache-2.0 双许可 | — | — |
| PostgreSQL 连接池 | 应用或驱动自带的连接池 | PgBouncer(实例多、连接数膨胀时;开事务池模式前按 DB-030 测试) | — |
| PostgreSQL 备份与时间点恢复 | pgBackRest · pgBackRest 为 MIT;WAL-G 为 Apache-2.0(其中 lzo 支持部分为 GPL-3.0+);Barman 为 GPL-3.0 | WAL-G(主要备份到对象存储、已在用 WAL-G 的团队) Barman(想要集中管理多台数据库的备份服务器;用 CloudNativePG 时走其 Barman Cloud 插件) | 只用 pg_dump:恢复不到任意时间点(DB-034) 只靠备库或存储快照:误删会被同步,快照不一定一致 |
| MySQL 备份与在线表结构变更 | Percona XtraBackup 加 binlog 归档;表结构变更先用原生在线 DDL | gh-ost 或 pt-online-schema-change(原生在线 DDL 不满足时;先审外键、触发器和复制的限制) | 认为备份工具自动支持新的 MySQL 主版本:要按工具的兼容表核实并做恢复演练 |
| PostgreSQL 高可用 | 按恢复目标分级:单机加异地备份 → 主备人工切换 → Patroni 自动切换 → Kubernetes 上用 CloudNativePG 先定 RPO/RTO(DB-033)再选级别 | — | 小规模单机系统直接上三节点自动切换集群:运维复杂度超过收益,且高可用不代替备份 只为跑数据库而引入 Kubernetes:多一套平台要运维 |
| 对象存储 | 单机或附件少时用本地文件系统加元数据表;多机时用 SeaweedFS · SeaweedFS 为 Apache-2.0;Garage 为 AGPLv3;MinIO 为 AGPLv3。分发和对外服务的义务口径见 27 | Garage(轻量、多地点自托管;先做 S3 功能 PoC,注意 AGPLv3) Ceph RGW(已有 Ceph 运维团队) | MinIO 社区版(新项目):官方仓库已于 2026-04-25 归档、不再维护,社区版不再提供预编译包;已有部署要规划迁移 自己写 S3 兼容服务:签名、分片、版本等协议细节难以做全 |
| 向量存储 | pgvector · pgvector 为 PostgreSQL License | Qdrant(压测后 pgvector 达不到要求,需要独立资源和带过滤的近似检索优化) Milvus(规模很大且团队能运维分布式系统) | 没有瓶颈就先上独立向量集群:多一套系统要同步、备份和运维 |
- PostgreSQL 迁移静态检查:能发现未用 CONCURRENTLY 建索引、未用 NOT VALID 加约束等风险写法;通过检查不等于没有锁风险,大表变更仍要人审(DB-016)。迁移执行工具按语言选,见 10–15。
- PostgreSQL 备份与时间点恢复:pgBackRest 的仓库曾于 2026-04 被维护者归档,2026-05 在多家公司赞助下恢复维护,2026-10 仍有正式发布;赞助模式可能再变,选用时关注其官网公告。
常见错误
待确认
下面的内容还没核实或待补调研,不要当成规则执行。
- PgBouncer、Patroni、Qdrant、Milvus、SeaweedFS、Garage、gh-ost、Percona XtraBackup 的许可证未从官方仓库 LICENSE 逐一核实(报告给出 PgBouncer 为 ISC、Patroni 为 MIT、Qdrant 与 Milvus 为 Apache-2.0、SeaweedFS 为 Apache-2.0、Garage 为 AGPLv3、gh-ost 为 MIT)
- pgBackRest 2026 年归档又恢复维护的经过只有二手报道(官网未见公告),赞助模式的长期稳定性待观察
- 报告称某个 PostgreSQL 小版本发布说明要求重做全量备份、重建部分索引,未核实原文
- MySQL 的新版本命名体系(LTS 与 Innovation 发布线)及 Percona XtraBackup 对新 MySQL 主版本的支持情况未核实
- CloudNativePG 内置备份改走 Barman Cloud 插件的现状未核实
- 达梦、金仓当前稳定版本与兼容范围未核实
- 待补调研:现成的 PostgreSQL 队列库(与 18 共同确认归属和默认推荐)
- 待补调研:恢复演练频率的业界依据(目前只是参考值),以及备份编排工具(与 26 衔接)
- 待补调研:对象存储上传的病毒扫描工具选型(与 25 衔接)
- 待补调研:MySQL 侧的隔离级别默认值(可重复读)对 DB-021、DB-022 的影响及间隙锁行为
来源
- S01 PostgreSQL: Versioning Policy · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-14
- S02 PostgreSQL: Transaction Isolation · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-14
- S03 PostgreSQL: SELECT(The Locking Clause) · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-14
- S04 pgvector · pgvector · 官方文档 · 核实于 2026-11-14
- S05 pgBackRest · pgBackRest · 官方文档 · 核实于 2026-11-14
- S06 WAL-G · WAL-G · 官方文档 · 核实于 2026-11-14
- S07 Barman · EnterpriseDB · 官方文档 · 核实于 2026-11-14
- S08 MinIO(GitHub 仓库) · MinIO · 官方文档 · 核实于 2026-11-14
- S09 Squawk · Squawk · 官方文档 · 核实于 2026-11-14
- S10 PostgreSQL: Row Security Policies · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-13
- S11 PostgreSQL: ALTER TABLE · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-13
- S12 PostgreSQL: CREATE INDEX · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-13
- S13 PostgreSQL: Explicit Locking · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-13
- S14 PostgreSQL: Continuous Archiving and Point-in-Time Recovery · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-13
- S15 PostgreSQL: Table Partitioning · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-13
- S16 PostgreSQL: UUID Functions · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-13
- S17 RFC 9562: Universally Unique IDentifiers (UUIDs) · IETF · 标准 · 2024-05 · 核实于 2026-11-14
- S18 PostgreSQL: Numeric Types · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-14
- S19 PostgreSQL: Error Codes · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-14
- S20 How to do distributed locking · Martin Kleppmann · 社区 · 2016-02-08 · 核实于 2026-11-14
- S21 ParallelChange · martinfowler.com · 社区 · 2014-05-13 · 核实于 2026-11-14
- S22 Pattern: Transactional outbox · microservices.io · 社区 · 核实于 2026-11-14
- S23 PgBouncer FAQ · PgBouncer · 官方文档 · 核实于 2026-11-13
- S24 MySQL: InnoDB Online DDL Operations · Oracle · 官方文档 · 核实于 2026-11-13
- S25 MySQL: Statements That Cause an Implicit Commit · Oracle · 官方文档 · 核实于 2026-11-14
- S26 PostgreSQL: Date/Time Types · PostgreSQL Global Development Group · 官方文档 · 核实于 2026-11-13
- S27 Qdrant · Qdrant · 官方文档 · 核实于 2026-11-13
- S28 Milvus · Milvus · 官方文档 · 核实于 2026-11-13
- S29 SeaweedFS · SeaweedFS · 官方文档 · 核实于 2026-11-13
- S30 Garage · Deuxfleurs · 官方文档 · 核实于 2026-11-13
- S31 Patroni · Patroni · 官方文档 · 核实于 2026-11-13
- S32 CloudNativePG: Barman Cloud Plugin · CloudNativePG · 官方文档 · 核实于 2026-11-13
- S33 Amazon S3: Sharing objects with presigned URLs · AWS · 官方文档 · 核实于 2026-11-13
- S34 gh-ost · GitHub · 官方文档 · 核实于 2026-11-13
- S35 TiDB: MySQL Compatibility · PingCAP · 官方文档 · 核实于 2026-11-13