第 30 章 / 共 57 章 · 约 3 分钟
关键技术:PostgreSQL
PostgreSQL 是系统设计面试里最稳妥的默认数据库:ACID 事务、约束、JOIN、各种索引都很成熟,单机能扛的量比很多人以为的大。除非题目给出明确理由(写入量远超单主、访问模式极简单、需要全文相关性排序),先选它,再说清楚什么时候要换。
1. 核心模型
- 关系模型和约束:主键、外键、唯一约束、CHECK 约束都由数据库保证。防止重复下单、重复订座,往往一条唯一约束就够了。
- MVCC:每次更新写出新版本,读不阻塞写、写不阻塞读。旧版本由 VACUUM 回收。
- 隔离级别:默认 Read Committed;Repeatable Read 在 PostgreSQL 里是快照隔离;Serializable 基于 SSI,冲突时让事务失败,由应用重试。
- 行锁:
SELECT ... FOR UPDATE锁住要改的行;FOR UPDATE SKIP LOCKED让多个 worker 从一张表里各取不同的任务,是用数据库做任务队列的常见写法。 - 索引:B-tree 覆盖大多数等值和范围查询;GIN 用于 JSONB 和全文检索;GiST 配合 PostGIS 做地理查询;BRIN 适合按时间追加的大表。
- WAL 和复制:所有修改先写预写日志(WAL)。流复制把 WAL 发给只读副本,默认异步,也可以配置成同步。
2. 什么时候用
- 需要事务和约束的核心数据:用户、订单、支付、库存、预订。
- 关系复杂、查询多变:后台报表、运营查询、需要 JOIN 的场景。
- 中等规模的一切:在合理的索引和硬件下,单个主库每秒几千到上万次写事务、更多的读,数据量到几 TB 都很常见。
- 一库多用:JSONB 存半结构化字段,PostGIS 做附近搜索,
tsvector做简单全文检索,SKIP LOCKED做任务队列,很多时候不用再引入新组件。
3. 什么时候不适合
- 写入远超单主能力:每秒持续几十万次写入(例如海量设备上报、聊天消息全量存储),单个主库撑不住,要么分片,要么换 Cassandra / DynamoDB。
- 全文相关性排序和复杂搜索:模糊匹配、同义词、按相关性打分,用 Elasticsearch。
- 大规模分析:在几十亿行上做聚合,用列式数仓(BigQuery、Snowflake、ClickHouse)。
- 跨地域多活写入:单主架构下,远地域写入要跨洋往返;需要多地域强一致写入时考虑 Spanner、CockroachDB 这类分布式 SQL。
4. 关键限制和数量级
| 维度 | 要记住的事 |
|---|---|
| 写入 | 单主;每秒几千到上万次写事务是常见量级,具体取决于事务大小和硬件 |
| 读扩展 | 加只读副本;异步复制有延迟(通常毫秒到秒级),刚写完马上读可能读不到 |
| 连接 | 每个连接是一个进程,几百个连接就要用 PgBouncer 之类的连接池,不能让上万个实例直连 |
| 数据量 | 单机几 TB 可以;更大或更快增长时,按时间做分区表、归档冷数据,或者分片(Citus、应用层分片) |
| 热点行 | 所有请求都更新同一行(全局计数器、热门商品库存)会在行锁上排队 |
5. 面试中常被追问的点
- 怎么防止同一个座位被卖两次? 最简单的是条件更新:
UPDATE seats SET status='held', hold_id=? WHERE id=? AND status='available',影响行数为 0 就说明被别人抢了;也可以用唯一约束或SELECT ... FOR UPDATE。详见票务抢购案例。 - 读副本的延迟怎么处理? 刚写完的用户短时间内读主库(read-your-writes),或者带上写入时的 LSN,副本追上后再读。
- 这个查询为什么慢? 看执行计划(
EXPLAIN ANALYZE),按 WHERE 和 ORDER BY 建复合索引,注意列的顺序。 - 什么时候分片、按什么分? 先用尽索引、缓存、读副本和分区表;分片键要让最常见的查询只落在一个分片上,例如按 tenant_id 或 user_id。
- 全局计数器成了热点? 拆成 N 行分片计数,读时求和;或者先在 Redis 或流处理里聚合,定期落库。
6. 和其他技术的取舍
| 选项 | 适合 | 代价 |
|---|---|---|
| PostgreSQL / MySQL | 事务、约束、灵活查询 | 写入受单主限制,分片要额外工作 |
| DynamoDB / Cassandra | 海量写入、按 key 访问 | 没有 JOIN,要按访问模式设计表 |
| Spanner / CockroachDB | 跨地域强一致的 SQL | 写延迟更高,成本和复杂度更高 |
| 列式数仓 | 大规模分析 | 不适合高频小事务 |
面试里的表述可以是:“先用 PostgreSQL,因为这里需要事务和唯一约束;如果写入量增长到单主撑不住,再按 X 分片或把这张表迁到宽列存储。”
7. 本站案例
- 案例 10:支付与账本:双分录账本、幂等键表、版本号乐观锁。
- 案例 12:票务抢购:用条件更新防止重复售座。
- 案例 6:通知系统:业务事务和 outbox 行写在同一个数据库事务里。