《数据库三剑客》讲清楚了 关系型数据库怎么用。本页补的是"单机关系型装不下时的那一整层":缓存、搜索引擎、分布式事务、分库分表。这些是系统从"能跑"走到"扛得住流量、保得住一致"的必经之路。我会像《Java 进阶》那样,把每个知识点拆成"它是什么 → 为什么这样设计 → 怎么用 → 踩过什么坑"。基线:Redis 7、Elasticsearch 8、MySQL 8 分库分表实践。
为什么关系型之外还需要别的存储
"一个 MySQL 走天下"在中小项目完全够用。但当数据量、并发、查询形态变得复杂,单一存储会同时被读写放大、热点、慢查询拖垮。于是出现"按访问形态选存储"的思路——本章先把视野打开。
一个 MySQL 走天下的天花板在哪
关系型库强在强一致事务 + 复杂关联查询,但它的代价也很清楚:行式存储、按主键/B树查、写要落盘还要保证 WAL。当下面任意一件事发生,单机 MySQL 就开始吃力:① 单表几千万行,B+树变深、查询变慢;② 读 QPS 上万、热点行被反复读,CPU 和连接被打满;③ 要跑 LIKE '%关键词%' 这种全表扫描的全文检索;④ 数据结构天天变,ALTER TABLE 要锁表。这些需求不是 MySQL 不努力,而是它的设计目标本来就不覆盖它们。
论为什么不能"硬扛"
很多人第一反应是"加机器、加索引、加从库"。但本质是用错了工具:让关系库去扛它不擅长的读放大/全文/灵活 schema,只会让主库越来越慢、运维越来越痛。换工具往往比加机器更划算。
多语言持久化:按访问形态选存储
"Polyglot Persistence" 是 Martin Fowler 提的概念:不同数据有不同的访问形态,就该用不同存储。订单要强一致 → 关系库;商品详情要扛读 → 缓存;搜索框要分词检索 → 搜索引擎;关系网络 → 图库。它们之间用应用层编排 + 异步同步串起来,而不是塞进一个库。
选型矩阵:谁擅长什么、谁不擅长什么
下面这张矩阵是后面所有章节的"地图"。看的时候记住:没有万能存储,只有"在这个维度上强"。
| 存储形态 | 擅长 | 典型代表 | 不擅长 |
|---|---|---|---|
| 关系型 | 强一致事务、复杂关联查询 | MySQL / PostgreSQL | 超大规模水平扩展、全文检索 |
| 缓存 KV | 亚毫秒读取、扛热点 | Redis | 复杂查询、持久化可靠性偏弱 |
| 文档库 | 半结构化、灵活 schema | MongoDB | 跨文档强事务、复杂 join |
| 搜索引擎 | 全文检索、聚合分析 | Elasticsearch | 强一致事务 |
| 列族 | 海量写入、宽表 | Cassandra / HBase | 随机读、事务 |
| 图 | 多跳关系查询 | Neo4j | 通用 CRUD |
| 时序 | 带时间戳指标 | InfluxDB | 事务、点查 |
一致性、延迟、成本的三角不可能
分布式存储有个隐藏约束:一致性(Consistency)、可用性(Availability)、分区容忍(Partition Tolerance)三者最多取其二(CAP)。网络分区不可避免,所以真实取舍是 CP(保一致、牺牲部分可用) 还是 AP(保可用、接受最终一致)。选存储前先想清楚:这条数据"几秒不一致会不会出大事"?
论选型的"三连问"
① 数据形状?结构固定还是多变?② 一致性要求?差几秒能不能忍?③ 读多写多、还是算多?问完,矩阵里基本只剩 1-2 个候选。别一上来就追"最潮"的数据库。
两个最高频的误区
误区一:缓存丢数据也无所谓。Redis 宕机或驱逐后,如果源库没同步好,用户看到"下单成功但查不到"——这就是把缓存当源的下场。误区二:把 ES 当主库。ES 近实时、不保证强一致、更新要整文档重索引,订单状态只存 ES 迟早出对账事故。铁律:强一致数据,关系库为源;缓存与 ES 都是副本。
本章落地的第一件小事
不用立刻推翻架构。先在你现有系统里标出三类数据:① 必须强一致的(账户、订单);② 读多写少可缓存的(商品详情、配置);③ 需要检索/分析的(日志、搜索)。这一步"分类"本身就是多语言持久化的起点。
引入新存储前,先算一笔成本账
新存储不是免费午餐。每引入一种,就要承担:机器与容量成本、监控告警、备份恢复、故障演练,以及团队学习曲线。小团队尤其要克制——一个运维良好、加好索引的 PostgreSQL,常常能扛住你以为"必须上 NoSQL"的量级。
| 隐性成本 | 说明 |
|---|---|
| 运维 | 部署、升级、容量规划、故障处理 |
| 监控 | 每种存储一套指标与告警 |
| 一致性 | 多副本同步、对账、幂等开发量 |
| 人力 | 团队要懂多种数据库的坑 |
论先优化,后加库
慢查询先加索引、大表先冷热归档、读多写少先上从库。这些零成本优化常能再撑一年。只有当瓶颈真实、优化无效,才引入对应专门存储,且要有明确 owner 与降级方案。别为了"架构看起来先进"而加库。
Redis 深入:它远不止是缓存
很多人把 Redis 当"缓存"用,但它是内存数据结构服务——字符串、哈希、列表、集合、有序集合(zset)、流(Stream)、位图都能直接用,对应不同业务语义。这一章把生产里最容易翻车的几块讲透。
先认全它的数据结构
Redis 的"快"除了因为内存,还因为每种结构都对应一个真实业务语义,不用自己拼。下面这张表是选型的基础。
| 结构 | 典型用途 | 坑点 |
|---|---|---|
| String | 缓存、计数器、分布式锁 | 大 value 阻塞 |
| Hash | 对象字段级读写(用户资料) | 别存超大字段 |
| List | 简单队列、最新 N 条 | 不是可靠队列 |
| ZSet | 排行榜、延迟队列、限流窗口 | score 精度 |
| Stream | 消息流、事件溯源 | 消费组概念 |
| Bitmap / HLL | 签到、UV 估算 | HLL 是近似值 |
持久化:内存数据怎么不丢
Redis 数据在内存,重启就丢?不会——它有持久化。RDB 是定时全量快照(恢复快、文件小,但可能丢最近一段);AOF 记录每条写命令(数据更全,但文件大、恢复慢);混合持久化(Redis 4+ 默认推荐)用 RDB 做基础 + AOF 增量日志,重启时先加载 RDB 再回放增量,兼顾速度与完整性。
论appendfsync 三档怎么选
always 每写都刷盘,最安全但最慢;everysec 每秒刷,丢最多 1 秒,生产默认;no 交给 OS,最快但可能丢很多。别为了"绝对不丢"上 always——除非你能接受吞吐腰斩。
缓存三大坑之一:穿透
穿透=查一个根本不存在的 key(如 id=-1),缓存没有、DB 也没有,请求每次都打到 DB,恶意刷接口时 DB 直接被打穿。解法两招:① 缓存空值(查不到也写个短 TTL 的 null,注意用布隆或特殊标记区分"真没有");② 布隆过滤器前置拦截——不存在的 key 在过滤器层就挡掉,根本不进 DB。
缓存三大坑之二三:击穿与雪崩
击穿:某个热点 key 过期的瞬间,海量并发同时发现缓存没了,一起打到 DB——一个 key 拖垮一片。雪崩:大量 key 同一时刻过期(或 Redis 整体宕机),DB 被瞬间冲垮。两者解法不同:击穿用互斥锁重建(只有一个线程去查 DB,其余等待)或逻辑过期(值里带过期时间,后台异步刷新,读永远不等待);雪崩用过期时间加随机抖动 + 集群高可用(Redis 挂了还有从/分片兜底)。
"逻辑过期"是缓存 key 永不真过期,value 里塞一个 expire 字段,读时若发现过期就触发异步重建。好处是读永远不卡;坏处是重建完成前返回的是旧数据。适合"数据稍微旧一点没关系"的场景(商品详情),不适合"余额必须实时"的场景。
| 坑 | 本质 | 典型解法 |
|---|---|---|
| 穿透 | 查不存在的 key | 布隆过滤器 / 空值缓存 |
| 击穿 | 热点 key 过期瞬间并发 | 互斥锁重建 / 逻辑过期 |
| 雪崩 | 大量 key 同刻失效/宕机 | 过期加抖动 / 集群高可用 |
分布式锁:SET NX / Lua / Redlock
用 Redis 做分布式锁,核心是 SET key value NX PX ttl:只有一个客户端能抢到(NX=不存在才设),并带唯一 value 和过期时间,避免"业务没跑完锁先过期"或"误删别人的锁"。释放锁必须用 Lua 脚本保证"判断+删除"原子,否则可能删掉别人刚抢到的锁。
主从异步复制时,主挂了锁还没同步到从,从升主后另一个客户端能再抢到同一把锁——锁丢了。要强一致用 Redlock(多节点 majority 抢锁,但有争议、实现复杂)或干脆 ZooKeeper / etcd(基于租约,更稳)。多数业务"最终一致 + 幂等"比"强一致锁"更划算,别为了锁而锁。
集群与高可用:主从 / 哨兵 / Cluster
单机 Redis 有容量上限(内存)和单点风险。主从复制:从库异步复制主库,做读写分离/备份;哨兵(Sentinel):监控主库,挂了自动选从库顶上(故障转移);Cluster:把数据按 16384 个槽位分片到多节点,支撑大数据量 + 高吞吐,节点间 gossip 互通。三者常组合:Cluster 内部每分片仍是主从 + 哨兵。
论内存淘汰:满了怎么办
Redis 达到 maxmemory 后按策略淘汰:noeviction(写报错,默认最稳)、allkeys-lru(全体 LRU,缓存常用)、volatile-lru(只淘汰带过期的)、allkeys-lfu(按访问频率)。做缓存选 allkeys-lru/allkeys-lfu;做存储(如会话)选 noeviction 并加告警,否则数据会被默默清掉。
大 key 与热 key:单线程的隐形炸弹
Redis 是单线程处理命令,一个慢命令会阻塞全实例。两颗雷:大 key(如存了百万元素的 Hash/List)删除或序列化极慢;热 key(某 key 被海量请求打)让单节点成为瓶颈。先定位再治理。
KEYS * 会遍历全库、阻塞单线程,线上一敲可能卡死服务。排查用 --bigkeys 或 SCAN 游标迭代。大 key 拆分(如按段 hash 成多个小 key),热 key 加本地缓存 + 多副本,别让一个 key 拖垮整机。
Elasticsearch:倒排索引与检索
关系型库的 LIKE '%关键词%' 是全表扫描 + 不支持分词,百万级就慢。ES 用倒排索引:先分词,再建"词 → 文档"的映射,检索时直接定位。这一章讲清它的原理、写入与"该当副本不该当源"。
倒排索引原理:把大海捞针变查字典
正排索引是"文档 → 内容"(一行一行存),查词要扫全部。倒排索引反过来:词 → 哪些文档包含它。比如文档 1 有"苹果手机"、文档 2 有"苹果电脑",分词后得到 term 字典:苹果 → [1,2]、手机 → [1]、电脑 → [2]。用户输入"苹果",直接命中文档 1、2,不用扫全文。
| Term | 文档列表 (docId) | 词频 |
|---|---|---|
| 苹果 | 1, 2 | 各 1 |
| 手机 | 1 | 1 |
| 电脑 | 2 | 1 |
论为什么搜索快
检索"苹果" = 在 term 字典里二分/哈希查到 posting list [1,2],O(log n)。再做相关度(TF-IDF / BM25)排序。这比"逐行 LIKE"快几个数量级,且天然支持高亮、聚合。
分词(Analysis):中文最容易被坑
分词把"iPhone 16 钛金属"拆成 [iphone, 16, 钛, 金属] 这些 term。ES 的分析链是 character filter → tokenizer → token filter。英文可以按空格切,中文必须用语义分词器(如 IK、jieba),否则默认会把每个字当 term,"苹""果"分开,搜"苹果"搜不到语义整体。映射里要显式指定 analyzer。
索引时用 ik_max_word(切得细),搜索时也用细的才能匹配;若搜索用 ik_smart(切得粗)反而更准(避免过召回)。索引 analyzer 与搜索 analyzer 要配套设计,否则"建了索引却搜不出"是中文 ES 最常见投诉。
写入流程与近实时(NRT)
ES 写入不是"写进去立刻能搜到"。流程:写请求进 内存 buffer → 刷到 translog(WAL,防丢)→ 默认 1 秒一次 refresh 把 buffer 变成可被搜索的段(segment) → 段多了后台 merge 合并 → 默认 30 分钟或 translog 太大时 flush 落盘。这就是"近实时":写入到可搜有约 1 秒延迟。
聚合分析(Aggregation)
ES 不只是搜,还能边搜边统计:terms 分组、avg/sum 度量、date_histogram 按时间桶。这让它成为日志分析(ELK)的主力——Kibana 的图背后全是 aggs。
论聚合跑到慢?多半是"深分页"或"宽字段"
terms 聚合的 size 太大、或对高基数字段(如 user_id)做 terms 聚合,会产生海量桶、吃内存。ES 7+ 默认有 search.max_buckets 限制。需要全量分组时,考虑用 composite 聚合做游标分页,而不是一次拉几万桶。
应作为只读副本:CDC 同步
正确姿势是关系库是源,ES 是只读检索副本。源数据变更时,通过 CDC(Change Data Capture)把 binlog/oplog 实时喂给 ES:Canal(MySQL)、Debezium(多源)、或应用双写。这样 ES 挂了不影响下单,ES 数据延迟几秒也能接受。
ES 写入有延迟、不保证强一致、更新成本高(常整文档重索引)。别把订单状态这种强一致数据只存在 ES。把 ES 当"搜索引擎 + 分析引擎",源永远在关系/文档库,靠 CDC 同步。这条线踩过的团队,没有不感谢当年听了这句话的。
集群、分片与容量规划
ES 数据按 index → shard(主分片)→ replica(副本) 组织。写入落到主分片、再同步副本;副本既提供高可用也分担读。分片数一旦设定很难改,所以建索引前就要估量:单分片建议 10–50 GB,太多分片 master 压力大,太少则无法水平扩展。
mapping 设计:字段类型选错会返工
ES 的 mapping 类似"表结构"。字段类型定错(如该用 keyword 的用了 text,或该用 scaled_float 的用了 float)会导致搜不出或精度问题,且改 mapping 常要重建索引。核心区分:text 分词检索、keyword 精确匹配/聚合、date 范围查询。
论text 还是 keyword?
要"包含某词"的全文检索用 text(会分词);要"完全等于某值"或做 terms 聚合/排序用 keyword。很多新人把状态字段设成 text,结果聚合出一堆分词碎片。一个字段可同时映射两种(fields 多字段),兼顾检索与聚合。
分布式事务:没有银弹
一个下单动作要同时扣库存、创建订单、减账户余额——它们在不同库甚至不同服务。单机 ACID 在这里失效,因为"事务边界"跨了节点。这一章讲主流方案与"为什么多数业务最终一致就够了"。
为什么单机 ACID 失效
本地事务靠"undo/redo log + 锁"保证要么全成要么全败。但跨服务时:服务 A 提交了自己的库,调用服务 B,B 失败了——A 已经提交了,没法回滚。网络还可能超时、丢包、重复,让"到底成没成"变成悬案。所以分布式下没有"真·原子提交",只有不同强度的近似方案。
论先问:真的需要分布式事务吗
很多"分布式事务"其实是设计问题:把本该一个聚合的数据拆到两个服务。先想能不能"合库、合并操作、用本地事务 + 幂等"解决。只有当数据天然跨域、又要求整体正确时,才上复杂方案。能不用就不用。
2PC:两阶段提交(强一致但危险)
协调者先发"准备":各参与者锁资源、写 undo/redo,回"就绪";都就绪再发"提交",否则全体回滚。问题是同步阻塞(准备阶段全程持锁)、协调者单点(它挂了参与者一直阻塞)、数据不一致风险(发了提交、部分参与者挂了)。所以 2PC 适合"库不多、要求强一致"的内部场景,互联网高并发很少裸用。
TCC:Try / Confirm / Cancel
TCC 把每一步拆成三个业务动作:Try(预留资源,如冻结库存而非真扣)、Confirm(真正提交,幂等)、Cancel(释放预留)。它不依赖数据库锁,而是业务层自己实现补偿,所以灵活、并发高,但侵入大——每个接口都要写三套逻辑,且 Confirm/Cancel 必须幂等。
网络重试会让 Confirm 被调用两次。若"扣库存"不是幂等,重复调用就多扣了。标准做法:用"已处理状态表"或业务版本号,确保同一动作多次执行结果一致。这是 TCC 上线翻车的第一原因。
Saga:长事务拆成一系列本地事务
跨多个服务的长流程(下单 → 支付 → 发货 → 通知),用 Saga:把大事务拆成一串本地事务,每步成功就走下一步;某步失败则反向补偿前面已成功的步骤。补偿顺序与正向相反("撤销链")。适合"最终一致可接受、流程长"的场景。
论编排(Orchestration)vs 协同(Choreography)
编排:有个中心协调器(如工作流引擎)驱动每一步,流程清晰、易观测,但中心是瓶颈。协同:各服务监听事件自己决定下一步,去中心但流程散、难追踪。长流程首选编排(Temporal / Cadence / 自研状态机),可观测性吊打协同。
本地消息表:最终一致 + 幂等实战
这是最常用、性价比最高的方案:把"写业务"和"发消息"放进同一个本地事务——业务表更新 + 消息表插入一条"待发送"记录,一起提交(保证"业务成了消息必在")。后台任务扫消息表投递到 MQ,消费者幂等处理;发送成功再标"已发"。即使 MQ 暂时不可用,消息表还在,不会丢。
本地消息表/MQ 一般保证"至少一次投递",即可能重发。若消费者"扣库存"不幂等,重复消息就多扣。幂等三件套:① 用唯一业务键(order_id)去重;② 消费前查"是否已处理";③ 或数据库唯一索引兜底。所有下游消费端都要有这层。
对账:最后一道兜底
再完美的方案也会出意外(消息丢了、消费挂了)。对账是兜底:定时拿"源数据"(订单)和"衍生数据"(账户变动、库存流水)比对,发现不一致就自动/人工修复。金融系统几乎都靠"T+1 对账"兜住实时链路的零星误差——它承认"实时不一定百分百准,但隔夜一定准"。
消息队列可靠性:不丢、不重、不乱序
本地消息表把消息交给 MQ 后,还要保证 MQ 这一环可靠。三个诉求:不丢(生产者确认 + 持久化 + 消费者手动 ack)、不重(靠消费者幂等,前面讲过)、不乱序(同 key 路由到同分区)。这层不稳,最终一致就垮。
| 风险 | 解法 |
|---|---|
| 消息丢失 | 生产者 confirm + broker 持久化 + 消费者手动 ack |
| 重复投递 | 消费者幂等(唯一键 / 去重表) |
| 顺序错乱 | 同业务键进同一分区 |
分库分表与读写分离
单表过千万行、单库连接打满,就该拆。读写分离解决"读多写少";分库分表解决"单表太大 / 单库容量上限"。这一章讲清分片键这个生死线,以及怎么平滑迁移。
什么时候该拆:先优化,再拆
别一慢就分库分表——它是最重的手段。顺序应该是:① 慢查询加索引;② 冗余字段/汇总表减少 join;③ 冷热分离(老数据归档);④ 读写分离;⑤ 才到分库分表。只有当单表行数上千万且增长快、或单库连接/容量到顶,才值得承受分片带来的复杂度。
| 手段 | 解决 | 代价 |
|---|---|---|
| 加索引 / 改 SQL | 慢查询 | 几乎无 |
| 冷热分离 | 历史数据拖慢 | 低 |
| 读写分离 | 读多写少 | 低(有一致性延迟) |
| 分库分表 | 单表/单库上限 | 高(跨分片痛) |
读写分离:主写从读
一主多从,写走主库,读走从库,分摊读压力。代价是主从延迟:刚写入的数据,从库可能还没同步到,"写完立刻读"会读到旧值。解法:写后强读的场景走主库;或引入"写后短暂读主"的路由规则。
分片键是生死线
分片键决定数据按什么规则落到哪个分片。选错,要么热点(数据全压一个分片),要么查询要广播所有分片。好的分片键:高基数(取值多)、贴合最高频查询维度、分布均匀。常见选 user_id / order_id 取模或哈希。
分片键一旦定下,历史数据按它分布,要换就得全量迁移 + 停写。所以上线前必须拿真实查询模式验证:"80% 的查询带哪个字段"。别凭感觉选。见过太多"上线半年发现热点全在一片"的血泪。
分片策略:范围 / 哈希 / 一致性哈希
范围分片(按时间/ID 段):简单、易范围查询,但新数据集中一片(热点)。哈希分片(取模):分布均匀、避免热点,但扩分片要重分布(取模基数变了)。一致性哈希:节点增减只影响邻近数据,扩缩容友好,是 Redis Cluster、Cassandra 的选择。扩分片前想清楚迁移成本。
| 策略 | 优点 | 缺点 | 适合 |
|---|---|---|---|
| 范围分片 | 易范围查询 | 新数据热点 | 时间有序、少写 |
| 哈希分片 | 分布均匀 | 扩分片要重分布 | 通用 |
| 一致性哈希 | 扩缩容只动局部 | 实现复杂 | 缓存/海量节点 |
跨分片查询痛点
分片后的噩梦:"按城市统计订单"这种不带分片键的查询,ES 里能落一片,分库分表里要广播到所有分片再汇总(scatter-gather),极慢且吃连接。解法:① 异构索引表(另建一张按城市的冗余表);② 冗余宽表;③ 把分析类查询交给 ES / 数据仓库,让分库分表只扛交易类带分片键的查询。
运营要的"全量多维统计"天生和反分片键。硬在分片库上 group by,就是让每个分片都扫一遍。正确分工:交易读写为王用分片库,分析报表交给 ES/OLAP(ClickHouse)。一个系统两种引擎,各管一摊。
平滑迁移:双写 + 灰度 + 校验
从单库迁到分片库,不能"停机切"。标准姿势:① 双写(新旧库同时写);② 新老查询灰度比对;③ 用脚本全量+增量同步旧数据;④ 校验两边一致后切读;⑤ 观察稳定再停写旧库。整个过程可回滚,出问题随时退回旧库。
全局唯一 ID:分库分表的前提
分了库,自增主键就冲突了(每个分片都从 1 开始)。需要全局唯一、趋势递增、可排序的 ID。雪花算法(Snowflake)用 41 位时间戳 + 10 位机器位 + 12 位序列号,单机每毫秒可生成 4096 个不重复 ID,且按时间有序,对 B+树索引友好。
若机器时钟往回拨(NTP 校正、虚拟机迁移),可能生成重复 ID。解法:检测到回拨就等待或借用上次序列、或用 ZK 分配 workerId 并校验。高并发系统生成 ID 要单独压测,别在事务里实时算。
选型决策与真实组合
前面是零件,这一章把它们装成一台机器。一个典型电商系统的存储组合,能说明"多语言持久化"怎么真正落地,以及背后的成本与运维权衡。
电商存储分工全景
看一个订单系统的全貌,每种存储只干最擅长的事,靠应用层 + 异步同步串起来。
| 数据 | 存储 | 为什么 |
|---|---|---|
| 订单 / 账户 / 库存 | MySQL(分库分表) | 强一致事务源 |
| 购物车 / 秒杀计数 / 登录态 | Redis | 扛热点读、分布式锁 |
| 商品搜索 / 日志检索 | Elasticsearch | 全文 + 聚合(只读副本) |
| 商品详情 / 评价草稿 | MongoDB | 结构多变、灵活 schema |
| 图片 / 静态资源 | 对象存储 + CDN | 分发给用户、便宜 |
| 监控指标 | 时序库 | 带时间戳指标 |
路它们之间怎么保持一致
下单写 MySQL(源)→ 发 MQ → 消费者更新 Redis(缓存)、同步 ES(检索)、减库存。每条链路都是最终一致 + 幂等 + 对账兜底。没有谁"实时强一致同步所有库",而是"源唯一、副本异步追"。
成本与运维权衡
每多一种存储,就多一套运维、监控、备份、告警、人员学习成本。不是越多越好。原则:能用一个关系库解决的,别上五个;只有当某个瓶颈真实存在且优化无效,才引入对应存储。引入前问"它的运维谁扛、挂了怎么降级"。
一致性与可用性的取舍实例
同一系统里不同数据取舍不同:余额、订单状态选 CP(宁可短暂不可用也不能错);商品浏览量、推荐选 AP(丢几个计数无所谓,不能让页面转圈)。别给所有数据定同一致性目标——那是浪费。按"出错代价"分级。
| 数据类型 | 取舍 | 容忍的不一致 |
|---|---|---|
| 账户余额 | CP | 几乎零 |
| 订单状态 | CP(源)+ 近似读 | 秒级 |
| 商品评分 | AP | 分钟级 |
| PV/UV 统计 | AP | 可估算 |
容量与监控基线
引入多种存储后,监控要覆盖各自健康度:MySQL 慢查询数 / 主从延迟;Redis 命中率 / 内存使用 / 大 key;ES 写入延迟 / 段数 / 堆;MQ 积压。没有监控的多存储架构,等于闭眼开车。关键指标设告警阈值,而非事后救火。
选型决策清单(给自己用的)
把前面所有内容压成一张"决策树",下次选型直接照着走:
论五步选型法
① 数据形状?结构化→关系库;嵌套多变→文档库;KV→Redis。② 一致性强吗?强→CP 关系库;可最终一致→放宽。③ 查询形态?全文→ES;多跳关系→图;时序→时序库。④ 规模?单表千万+→分片。⑤ 运维扛得住吗?新存储要有 owner、监控、降级。五步走完,答案自然出来。
反模式总结
① 把缓存当源(丢数据);② 把 ES 当主库(对账事故);③ 分片键拍脑袋(热点);④ 分布式事务裸用 2PC(阻塞+单点);⑤ 用分片库跑报表(全分片扫描)。这五条,本页每一章都在劝你别踩。
多活与容灾:存储如何跨机房
单机房有断电/断网风险。多存储架构的容灾策略各不相同:关系库用主从跨机房 + 半同步(丢数据少但延迟高);Redis 用跨机房复制或只读副本;ES 跨机房靠快照恢复 + CDC 重放。核心是"副本在异地、故障能切、数据能补"。
| 存储 | 容灾手段 |
|---|---|
| MySQL | 跨机房半同步主从 / MGR |
| Redis | 跨机房副本 + 本地读 |
| ES | 快照 + CDC 异地重建 |
| etcd | 跨 AZ 部署(奇数节点) |
论容灾等级决定成本
同城双活、异地多活、两地三中心,级别越高越贵越复杂。先定 RTO/RPO(恢复时间 / 数据丢失目标),再选级别。多数业务"同城主从 + 定期备份 + 演练"已够;金融核心才上异地多活。别为"听起来稳"上超出预算的容灾。
关系型之外按访问形态选存储(多语言持久化)。Redis 是内存数据结构服务:RDB/AOF/混合持久化保内存不丢,缓存三大坑(穿透→布隆/空值、击穿→互斥锁/逻辑过期、雪崩→抖动+集群)、分布式锁用 SET NX + Lua、高可用靠主从/哨兵/Cluster。Elasticsearch 用倒排索引做检索、分词器要配套、写入近实时、应作只读副本靠 CDC 同步。分布式事务没有银弹:2PC 强一致但阻塞、TCC 业务补偿、Saga 长流程、本地消息表 + 幂等 + 对账最常用。分库分表分片键是生死线、跨分片查询要交给 ES/OLAP、迁移走双写灰度。落地核心:源唯一、副本异步追、按出错代价分级一致性、每个存储有监控有降级。
1.缓存穿透、击穿、雪崩三者区别是什么?分别怎么防?
查看答案
穿透=查根本不存在的 key,每次都打 DB(防:布隆过滤器 / 缓存空值);击穿=热点 key 过期瞬间并发打 DB(防:互斥锁重建 / 逻辑过期);雪崩=大量 key 同刻过期或 Redis 宕机(防:过期加随机抖动 / 集群高可用)。
2.用 Redis 做分布式锁,为什么释放锁要用 Lua 脚本?单实例 Redis 当锁有什么隐患?
查看答案
释放时要"先判断 value 是自己的、再删除",这两步必须原子,否则可能删掉别人刚抢到的锁——Lua 保证原子。单实例主从异步复制时,主挂锁未同步,从升主后别人可再抢同一把锁(锁丢失);强一致场景应换 etcd/ZK 或 Redlock,多数业务用"最终一致+幂等"更划算。
3.为什么 ES 不能当主数据库用?正确姿势是什么?
查看答案
ES 近实时(写入到可搜有秒级延迟)、不保证强一致、更新要整文档重索引、运维重。正确姿势:关系库为强一致源,ES 为通过 CDC(Canal/Debezium)同步的只读检索/分析副本。订单状态这类强一致数据只存 ES 必出对账事故。
4.分库分表最该先想清楚的是什么?选错会怎样?
查看答案
分片键。它决定数据分布与最高频查询能否落在一个分片内。选错(如用时间)会造成热点且上线后极难更改——要换就得全量迁移+停写。上线前须用真实查询模式验证"80% 查询带哪个字段"。
5.分布式事务里"本地消息表"为什么最常用?它依赖什么前提?
查看答案
因为它把"写业务+发消息"放进同一本地事务,保证业务成了消息必在,后台投递到 MQ,性价比高、不阻塞。前提是消费者必须幂等(至少一次投递会重发),且要有对账兜底最终一致。强一致核心链路才上 TCC/2PC。
下一步往哪走
路学完数据库进阶之后
① 动手搭一套:本地用 Docker 起 Redis + MySQL 主从 + ES,写个带缓存、防穿透、异步同步 ES 的接口,把本章链路跑通。
② 看 NoSQL 页:本页 Redis/ES 偏"用法与坑",NoSQL 页补 MongoDB/Cassandra/Neo4j 等其它形态,拼齐多语言持久化全景。
③ 看系统设计与 DevOps:缓存、分片、限流、可观测都是系统设计的拼图,配合《系统设计》一起学才能落地架构。
④ 深入可观测:慢查询、缓存命中率、ES 写入延迟——这些要靠监控才能调优,别等线上炸了才看。