主题
数据层设计:分库分表、NoSQL 与搜索引擎
本文是系统设计系统学习系列的 L2 核心篇。前置:29. building-blocks-lb-cache-queue-cdn。 学完可以配合面试题食用:10-search-engine-elasticsearch、06-分布式ID生成器
为什么数据层需要单独设计
业务逻辑是代码,数据是状态。代码可以随时重写,状态丢了就丢了。所以数据层是所有架构决策中最保守的一环——换数据库比换语言版本难一个数量级。
选存储的本质是选一致性模型和查询能力的取舍。关系型数据库给 ACID,但横向扩展困难;KV 给高性能和高可用,但查不了多条件;搜索引擎给全文检索,但一致性弱。没有银弹,只有决策树。
存储选型决策树
拿一个需求来拉决策树:假设要做一个文章系统,支持分页列表、全文检索、按作者统计、评论事务。
- 关系型(MySQL):事务、JOIN、二级索引。适合需要强一致和复杂查询的场景,比如评论(事务)、作者统计(GROUP BY)。但千万级文章列表页,分页偏移量大了就慢。
- KV(Redis/Cassandra):按 key 读,延迟毫秒级。适合"主键查"或"简单二级索引",文章详情页可以放 Redis 缓存。但多条件组合查询就无能。
- 文档型(MongoDB):不需要预定义 schema,适合 JSON 结构灵活的数据。文章本身可以存文档库,但别指望它做 JOIN。
- 列存(ClickHouse/HBase):列式存储,适合分析型查询(聚合、扫描大表)。作者统计分析可以丢给列存,实时性要求不高。
- 搜索引擎(Elasticsearch):倒排索引,适合全文检索和多条件组合。文章正文搜索、多标签筛选,它最擅长。但别拿来当主存储——ES 的写入和一致性问题比 MySQL 多。
决策口诀:事务 → 关系型,简单海量 → KV,灵活 schema → 文档,分析 → 列存,搜索 → 倒排。 复杂系统通常组合 2-3 个覆盖。
MySQL 扩展路径:从索引到换存储
一个 MySQL 实例从 10 万行到 10 亿行,经历的阶段(呼应 mysql 系列第 35 篇):
- 索引优化(百万级):加索引、覆盖索引、慢查询优化。这个阶段不用动架构,收益最高。
- 读写分离(千万级):主库写,从库读。主从延迟是主要问题,业务接受写后读主的方案。
- 分库分表(亿级):按用户 ID 或订单号分片。分片键选错,后面全完蛋。跨分片查询(比如全量报表)需要走汇总层。
- 换存储(百亿级):如果查询模式变了(比如从"按用户查"变成"按标签聚合分析"),可能列存或搜索引擎更适合。这时候关系型数据库只做事务操作,分析查询走别的存储。
每个阶段都有明确的触发信号。索引优化阶段不要上来就分库分表,分库分表的运维成本是索引优化的 10 倍以上。
分片设计核心问题
分片键选择
分片键决定了 90% 的查询性能。选错就是灾难。
- 好分片键:访问均匀 + 大多数查询带这个键。用户 ID 是常见选择,因为大部分查询按用户维度。
- 坏分片键:主键自增(写入打点到尾端)、时间戳(热点集中在最新数据)。
选分片键的衡量标准:95% 以上的查询能在一个分片内完成。如果大量查询需要跨分片汇总,说明分片键选错了。
跨分片查询
分片之后,ORDER BY + LIMIT + OFFSET 变得复杂。比如分 16 片,每片取前 20 条,全量排序后只返回 20 条——每片要取 20 条,因为数据可能全在其中一片里。实际做法是每片取 20 条,汇总后按全局排序取前 20。OFFSET 越大,代价越高。
全局唯一 ID
分片后不能再用自增主键,需要全局唯一 ID 生成器。三种主流方案:
| 方案 | 主键含义 | 是否有序 | 核心问题 |
|---|---|---|---|
| 雪花算法 | 时间戳+机器+序号 | 趋势递增 | 时钟回拨 |
| 号段模式 | 业务号段+自增 | 连续递增 | 号段分配器单点 |
| UUID | 随机 | 无序 | 非递增,MySQL 页分裂 |
雪花算法用得最多:64 位(1 位符号 + 41 位时间戳 + 10 位机器 + 12 位序列号),单机每毫秒 4096 个 ID,够用且有序。
基因法分片:双维度查询
如果分片键是 user_id,但业务需要按 order_id 查,怎么办?基因法把分片键的"基因"编码到 order_id 里,使得 order_id 能直接算出它在哪个分片。
java
// 分片数必须是 2^n,这里用 16 片(4 位)
public class ShardRouter {
private static final int SHARD_COUNT = 16;
private static final int SHARD_BITS = 4; // 2^4 = 16
// 生成带分片基因的 orderId
public static long generateOrderId(long userId, long seq) {
int shard = (int) (userId % SHARD_COUNT); // 确定分片
long timestamp = System.currentTimeMillis();
// 拼接:timestamp(42位) + seq(18位) + shard(4位)
return (timestamp << 22) | (seq << 4) | shard;
}
// 从 orderId 反推分片
public static int getShardByOrderId(long orderId) {
return (int) (orderId & (SHARD_COUNT - 1));
}
// 按 userId 分片(常规查询)
public static int getShardByUserId(long userId) {
return (int) (userId % SHARD_COUNT);
}
}核心思路:orderId 的低 4 位就是分片号,拿到 orderId 就能直接定位。不用查映射表,不用广播。
冷热分离与归档
数据有温度。刚产生的数据热,访问频繁;三个月后变温,偶尔查;一年后变冷,归档即可。
- 热数据(SSD 存储):最近 3 个月,活跃读写。3 副本,延迟 < 5ms。
- 温数据(HDD 或更低成本的 SSD):3 个月到 1 年,只读或低频读。可以迁移到只读从库。
- 冷数据(对象存储 S3/OSS):超过 1 年,合规归档。按日期分区存成 Parquet 文件,查询时从对象存储扫描。
成本差约一个数量级:SSD 每 GB 约 $0.15/月,HDD 约 $0.03/月,对象存储约 $0.01/月。
冷热分离的典型做法:双写。热库写一份,同时异步写一份到对象存储(或列存)。查询先走热库,冷数据走归档层。业务上对用户做限制——"只能查最近 3 个月的订单"。
Elasticsearch 什么时候上
ES 不是万能搜索。以下场景才适合:
- LIKE 检索:MySQL 的
LIKE '%keyword%'不走索引,全表扫描。ES 的倒排索引天生全文检索。 - 多条件组合:价格区间 + 标签筛选 + 关键词搜索,MySQL 组合索引很难覆盖所有排列。ES 的 filter 查询组合任意字段。
- 相关性排序:全文检索需要按 TF-IDF 或 BM25 评分排序,MySQL 做不了。
ES 的禁忌:
- 不要拿 ES 当主存储:ES 的写一致性是最终一致,写入失败可能丢数据。主数据必须存 MySQL 或 KV,ES 只做索引。
- 不要频繁更新:ES 的更新是删除+新增,段合并代价高。高频更新场景(比如计数器)别用 ES。
存储选型决策表:
| 场景 | 主存储 | 辅助存储 | 典型方案 |
|---|---|---|---|
| 核心交易(订单、支付) | MySQL | Redis 缓存热点 | 分库分表 + 双写归档 |
| 内容管理(文章、商品) | MySQL + ES | CDN 缓存静态资源 | Canal 同步 MySQL → ES |
| 日志分析 | 对象存储 | ClickHouse | Flume → Kafka → 列存 |
| 用户行为(Feed 流) | KV + 列存 | Redis 缓存关系 | Cassandra 存关系,Redis 存热 Feed |
| 简单海量数据 | KV | - | 直接选 Cassandra/DynamoDB |
常见误区与小结
- 分片键用时间戳:新数据集中在最近几个分片,写入打点,查询热点,分片失去了意义。选均匀分布的键。
- 分库分表后不做跨分片查询兜底:总有业务需要全量统计,提前设计好汇总层(比如用 Canal 同步到列存,再出报表)。
- ES 做主存储:ES 的写入是缓冲 + 批量 flush,宕机可能丢最近几秒的数据。主存储必须选持久化更可靠的方案。
- 冷热分离留到最后做:数据量大到一定规模,冷热分离的迁移成本极高。设计初期就规划好数据生命周期。
- 选存储时只看性能不看一致性:Redis 每秒 10 万 QPS 很诱人,但如果业务需要读自己写的最新数据,Redis 的异步复制可能读到旧数据。
小结:数据层设计没有银弹,存储选型的核心是画出业务查询模式,然后按模式匹配最合适的存储。一个系统用好 2-3 个存储是常态,别指望一个 MySQL 或一个 ES 搞定所有。下一篇会讲高性能套路(异步、池化、水平扩展),把数据层选好之后,再考虑怎么让整体架构跑得快。
参考
参考:MySQL 官方文档 - Sharding | Elasticsearch 官方指南 - 倒排索引 | 数据库存储选型决策树(DDIA 第 6-7 章)