大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
数据架构
这一课你将能做到
- 用 CAP 定理解释「为什么网络分区时必须在一致性和可用性之间二选一」
- 为本站设计缓存方案:选型 Redis、确定缓存模式、规避缓存穿透/击穿/雪崩
- 判断一个业务场景该用 MySQL、Redis、MongoDB 还是 Elasticsearch,并给出理由
为什么要学
数据是系统里最「重」的东西——代码可以重构,服务可以重写,但几 TB 的数据迁移一次伤筋动骨。数据架构选错,后期纠正的代价是架构中最高的。
人话:选数据库就像选仓库。MySQL 是带账本的管理严格的仓库(入库慢但账目清晰),Redis 是店门口的快取货架(拿得快但地方小、可能丢),MongoDB 是什么都往里扔的大平层(灵活但找东西费劲)。
先建立图像
本站数据架构演进:
【阶段一】 【阶段二】 【阶段三】
App ──→ MySQL(单库) App ──→ Redis ──→ MySQL 订单服务 ──→ MySQL(订单库)
(缓存) (主→从) 课程服务 ──→ MySQL(课程库)
搜索服务 ──→ Elasticsearch
行为埋点 ──→ Kafka ──→ ClickHouse
CAP 定理图解:
C(一致性)
/ \
/ \
/ \
A ────── P
可用性 分区容错性
网络分区必然发生(P 必须选),所以只能选 CP 或 AP:
- CP:ZooKeeper、HBase、etcd —— 宁可不可用,不能不一致
- AP:Cassandra、DynamoDB、Redis 主从 —— 宁可旧数据,不能不可用
跟着做
步骤 1:用 ACID 分析订单场景
本站订单表设计:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
version INT NOT NULL DEFAULT 0, -- 乐观锁
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_status (user_id, status)
) ENGINE=InnoDB;
ACID 逐条落地:
- 原子性:下单 = 插订单 + 扣库存,用本地事务包住
- 一致性:
status只能是枚举值,用 CHECK 或应用层校验 - 隔离性:默认 REPEATABLE READ,热点行用乐观锁 version 字段
- 持久性:
innodb_flush_log_at_trx_commit=1, redo log 落盘
步骤 2:缓存三大经典问题实战
本站课程详情接口(QPS 500)加 Redis 缓存:
// 缓存穿透:查不存在的课程 id,请求打到 DB
public Course getCourse(long id) {
String key = "course:" + id;
Course c = redis.get(key);
if (c != null) return c;
c = db.findById(id);
// 关键:空值也缓存 60 秒,防穿透
redis.set(key, c == null ? NULL_PLACEHOLDER : c,
c == null ? 60 : 3600);
return c;
}
| 问题 | 现象 | 本站解法 |
|---|---|---|
| 穿透 | 恶意刷不存在的 id | 空值缓存 + 布隆过滤器 |
| 击穿 | 热点 key 过期瞬间大量请求打 DB | 互斥锁(SETNX)单线程回源 |
| 雪崩 | 大量 key 同时过期 | 过期时间加随机抖动(3600 ± 300 秒) |
步骤 3:缓存模式选型
Cache-Aside(本站采用):
读:先读缓存 → miss → 读 DB → 写缓存
写:先写 DB → 删缓存(不是更新!)
Read/Write-Through: 缓存代理 DB,应用只跟缓存说话
Write-Behind: 先写缓存,异步批量刷 DB(快但可能丢)
面试必答点:为什么写操作是删缓存而不是更新缓存? 因为更新可能并发错乱(A 写 DB → B 写 DB → B 更新缓存 → A 更新缓存,缓存里反而是旧值),删除配合延迟双删更安全。
讲清楚要点
- CAP 的真实含义:分区容错 P 在分布式系统里必须保证,真正的选择是分区发生时选 C 还是选 A。正常无分区时两者可兼得。
- BASE 是对 ACID 的工程妥协:基本可用(Basically Available)、软状态(Soft state)、最终一致(Eventually consistent)。本站的课程浏览量就是 BASE 场景——丢几百次计数无所谓。
- NoSQL 选型看数据模型:KV 选 Redis、文档选 MongoDB、列式选 ClickHouse/HBase、图选 Neo4j、搜索选 ES。不要用 MongoDB 模拟关系表。
- 索引是 MySQL 性能的第一杠杆:B+ 树索引、覆盖索引、最左前缀。本站
idx_user_status支撑「我的订单列表」查询,避免全表扫描。 - 分库分表是最后手段:能用索引解决就别分表,能用分表解决就别分库。分库分表引入分布式事务、跨片 join、全局 ID 一系列问题。
你可能会懵的地方
-
Redis 这么快,能不能直接当主库? 不能。Redis 的持久化(RDB 快照/AOF 日志)可靠性远低于 InnoDB,内存成本高,且缺乏复杂查询能力。它定位是缓存和轻量存储(Session、计数器、排行榜)。
-
最终一致性,业务怎么接受? 用「读己之写」兜底:用户刚改完的资料强制读主库或写缓存,其他用户看到旧数据无所谓。把一致性要求细化到场景,而不是整个系统一刀切。
-
MySQL 和 PostgreSQL 怎么选? 两者都足够好。MySQL 生态大、中间件多、国内招聘容易;PG 功能强(JSONB、GIS、CTE)。团队熟悉度优先,本站选 MySQL 因为 DBA 资源和云上 RDS 成熟。
-
数据仓库和数据库的区别? 数据库(OLTP)面向事务:小批量、高频、实时;数仓(OLAP)面向分析:大批量、低频、离线。本站订单在 MySQL(OLTP),用户行为日志进 ClickHouse(OLAP)做漏斗分析。
-
要不要上 Hadoop/Spark? 看数据量。日增量低于 1TB 没必要上 Hadoop,用 Kafka + Flink 或直接用 ClickHouse 就够。大数据平台运维成本极高,小团队会被拖垮。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 缓存和 DB 双写更新 | 并发下缓存是脏数据 | Cache-Aside:写 DB 后删缓存 |
| 所有 key 过期时间设成一样 | 同时过期引发雪崩 | 基础时间 + 随机抖动 |
| 一张表 50 个字段还继续加 | 行太长,查询和索引效率都差 | 垂直拆分:主表 + 扩展表 |
| 用 MySQL 存用户行为日志 | 写入洪峰拖垮主库 | Kafka 削峰 → ClickHouse |
| 为了「以后分库」提前用分布式 ID | 增加复杂度却用不上 | 初期自增 ID 够用,分库时再迁移 |
卡点急救
- 慢查询拖垮数据库:先
SHOW PROCESSLIST找元凶,再EXPLAIN看执行计划,90% 的慢查询是缺索引或索引失效(函数操作、隐式转换、前导通配符)。 - Redis 内存爆了:
INFO memory看占用,检查是否有大 key(--bigkeys),设置 maxmemory-policy 为 allkeys-lru,给所有 key 加 TTL。 - 主从延迟导致读到旧数据:关键读(支付后查订单)强制走主库,非关键读走从库;或监控延迟超过阈值时自动切主。
面试常问
-
CAP 定理怎么理解?能同时满足吗? 网络分区(P)在分布式系统中必然发生,因此只能在一致性(C)和可用性(A)之间权衡:CP 系统在分区时拒绝服务保证一致(如 ZooKeeper,选主期间不可用);AP 系统分区时继续服务但可能返回旧数据(如 Redis 主从切换间隙)。无分区时 CA 可兼得。举例本站:订单服务选 CP(钱不能错),课程浏览量选 AP(数字晚点更新无所谓)。
-
缓存穿透、击穿、雪崩的区别和解决方案? 穿透:查不存在的数据,缓存和 DB 都没有——解法:空值缓存 + 布隆过滤器。 击穿:热点 key 恰好过期,并发回源打爆 DB——解法:互斥锁单线程回源,或热点数据不过期。 雪崩:大量 key 同时过期或 Redis 宕机——解法:过期时间加随机值、多级缓存、熔断降级。 加分项:说清楚三者的根因都是「缓存失效后请求直达 DB」。
-
MySQL 索引为什么用 B+ 树? 对比答题:哈希索引不支持范围查询;二叉树太高导致 IO 次数多;B 树非叶子节点存数据,每页能放的 key 少;B+ 树非叶子只存 key,3 层就能存千万级数据,且叶子节点链表天然支持范围扫描。结合 InnoDB 聚簇索引讲:主键索引叶子存整行数据,二级索引叶子存主键值(回表)。
自测三问
- 本站的「课程搜索」功能数据量到 50 万后,该从 MySQL 迁移到什么?同步方案怎么做?
- 写操作为什么推荐「删缓存」而不是「更新缓存」?并发场景下还有什么坑?
- 如果本站要做「用户学习漏斗分析」(浏览→试听→购买),原始数据应该存哪里?
小结一句话
数据架构的核心是按「一致性要求、读写比例、数据量级」三要素匹配存储引擎,并用缓存模式保护最脆弱的数据库层。
延伸
- 《数据密集型应用系统设计》(DDIA, Martin Kleppmann)——数据架构圣经
- MySQL 官方文档:InnoDB 索引章节
- Redis 官方文档:缓存模式(cache patterns)
- 下一课:API 与集成——REST、GraphQL、gRPC 与消息队列的选型。