← 学习门户 每日面试题 学习看板

2026-10-21

数据库与存储 —— 从慢查询/连接池到索引、热点写与主从切换

10 题 · 平均 3.2/5 · low 3 · medium 3.25 · high 3.33 · 第 24 轮

#1 什么是慢查询日志?它能帮助定位哪些问题? low practice

题目

什么是慢查询日志?它能帮助定位哪些问题?

我的回答

慢查询日志是 MySQL 中用于定位 SQL 执行缓慢问题的。开启后可以设置时间阈值,用是否使用索引来控制记录内容。比如设置超过 500 毫秒的 SQL 就记录到慢查询日志里。后续可以通过命令获取慢查询日志,之前主要是直接在管理后台看,具体命令没印象。它主要定位系统里有哪些 SQL 存在性能问题、哪些 SQL 没有用到索引,然后针对慢 SQL 做优化。

点评

优点:开关、阈值、是否记录未走索引三条主线都在,定位目标明确。
不足:没讲慢日志字段与分析方法;没提 `log_output`、`long_query_time=0` 的风险。
补充:重点看 `Query_time / Lock_time / Rows_sent / Rows_examined`,用 `mysqldumpslow` 或 `pt-query-digest` 按总耗时排序;`Rows_examined/Rows_sent` 比值大说明扫描多,`Lock_time` 高说明锁等待。

完整讲解

第一部分:知识重点剖析


核心考点是「可观测性入口」:能不能把慢日志变成可执行的优化清单。三个开关——`slow_query_log`(总开关)、`long_query_time`(阈值,建议 0.1~0.5s)、`log_queries_not_using_indexes`(抓未走索引,注意刷屏)。落地三件事:开 `log_output=FILE` 并轮转、用 `pt-query-digest` 聚合、按 `Rows_examined` 与 `Lock_time` 分类归因。

第二部分:考察方向与延伸递进


- 深度:慢日志与 `performance_schema.events_statements_summary_by_digest` 的区别与互补
- 广度:慢日志 → 执行计划 → 索引设计 → 表结构治理
- 应用:线上突发慢查询,如何在不改代码的情况下先止血
1. 慢日志里 `Rows_examined` 很大但 `Rows_sent` 很小,说明什么?——扫描放大,优先补索引或改写法。
2. `long_query_time` 设 0 有什么风险?——全量记录,IO 放大,只能临时采样。
3. 为什么慢日志抓不到锁等待?——`Lock_time` 只统计等表锁/行锁的部分时间,长事务更多体现在业务 RT。

第三部分:面试者参考回答


**【一句话结论】** 慢查询日志是把「用户说慢」翻译成「哪条 SQL、慢在扫描还是慢在等锁」的第一入口。
**【三点展开】** 第一,三个开关:总开关、时间阈值、是否记录未走索引,线上阈值一般 0.1~0.5 秒。第二,拿到日志先聚合再排序,按总耗时排,不按单次耗时排。第三,看 `Rows_examined` 和 `Rows_sent` 的比值判断是扫描放大,看 `Lock_time` 判断是不是锁等待。
**【一句边界】** 慢日志只覆盖执行阶段,连接建立、网络传输、应用侧排队要靠连接池指标和链路追踪补。

#2 数据库连接池的作用是什么?核心参数有哪些? low practice

题目

数据库连接池的作用是什么?核心参数有哪些?

我的回答

数据库连接池的作用是管理数据库连接,通过复用连接快速执行 SQL、快速反馈数据,防止每次建立新连接浪费时间和资源。连接被打满之后,整个数据库会出现无法访问、无法使用的情况,因为没有连接给其他服务用了。之前出现过类似情况:一个超大 SQL 加上并发量上来,把连接都占满,导致系统宕机,后来临时把接口注掉才恢复。核心参数是最大连接数、回收时间。

点评

优点:连接复用与打满后果都有实感,案例讲得清楚。
不足:把「连接池打满」说成「数据库不可用」不准确——是应用线程阻塞在 `getConnection()`,超时抛异常,进而拖垮业务线程池;参数只报了两个。
补充:`minimumIdle / maximumPoolSize / connectionTimeout / idleTimeout / maxLifetime(< DB wait_timeout)/ leakDetectionThreshold`;池大小要匹配 DB `max_connections` 与后端真实并发。

完整讲解

第一部分:知识重点剖析


连接池本质是「有界资源池 + 排队 + 超时熔断」。复用省掉 TCP 握手与认证开销;`maximumPoolSize` 是限流阀,保护数据库;`connectionTimeout` 把排队失败快速暴露;`maxLifetime` 短于服务端 `wait_timeout`,避免拿到被 MySQL 单方面关闭的死连接;`leakDetectionThreshold` 抓未归还连接。

第二部分:考察方向与延伸递进


- 深度:HikariCP 为什么快(无锁 ConcurrentBag、FastList)
- 广度:连接池 → 线程池 → 全链路超时预算
- 应用:池打满时如何区分「慢 SQL 占坑」与「泄漏」
1. 池大小怎么定?——先按 `QPS × RT` 估并发,再压测校准,不是越大越好。
2. 连接泄漏怎么发现?——`leakDetectionThreshold` + 池活跃数长期贴顶 + 堆栈采样。
3. 数据库 `max_connections` 与池上限的关系?——所有实例池上限之和必须留余量给运维与备份连接。

第三部分:面试者参考回答


**【一句话结论】** 连接池是数据库前面的有界缓冲,用复用省开销,用上限保数据库。
**【三点展开】** 第一,作用是复用连接、限流保护,避免每次请求都建连。第二,核心参数是最大连接数、最小空闲、获取超时、空闲回收和最大存活时间,最大存活要小于数据库的 wait_timeout。第三,池打满不是数据库挂了,是应用线程卡在获取连接,超时后异常上抛,进而把业务线程池拖满,形成雪崩。
**【一句边界】** 连接池只能保护数据库,救不了慢 SQL;慢 SQL 占满连接时,先限流和杀慢查询,再谈扩容。

#3 什么是数据归档,一般是怎么做的? low practice

题目

什么是数据归档,一般是怎么做的?

我的回答

数据库归档主要分几种情况。首先是冷备,对应数据基本不再访问,只有特殊要求时才用。大表会做分区表,把对应分区导出做备份,存到对象存储里冷备。如果需要归档后还能查询,之前的方案是把冷数据从数据库迁移到 ES,用 DataX 做异构迁移,也可以做计划任务分批迁移,时间选在业务不繁忙的时候。

点评

优点:分区表 + exchange 导出冷备、DataX 迁 ES 做可查询归档、避开高峰,三条路径都对。
不足:子问题 2、3 没答——归档表索引怎么留、`pt-archiver` 如何控速防主从延迟。
补充:归档表同结构但只留主键 + 查询维度索引;`pt-archiver` 用 `--limit`、`--txn-size`、`--sleep`、`--max-lag` 按主键分批,禁大事务大 DELETE。

完整讲解

第一部分:知识重点剖析


归档的本质是「把访问频次低的数据从热存储挪到冷存储,同时保证可追溯」。三条常见路径:分区表 `EXCHANGE PARTITION` 秒级摘分区;分批迁移到归档表/对象存储;异构进 ES/ClickHouse 支持检索。共同约束是不影响线上:按主键小批量、控制事务大小、感知主从延迟、避开高峰。

第二部分:考察方向与延伸递进


- 深度:分区表与分表的区别、`EXCHANGE PARTITION` 的元数据操作代价
- 广度:冷热分层 → 生命周期管理 → 合规保留期
- 应用:归档后业务仍要查历史,如何做透明路由
1. `pt-archiver` 怎么避免主从延迟?——`--max-lag` 检测延迟自动暂停,`--sleep` 主动让路。
2. 归档表要不要保留原索引?——只保留查询必需的,索引越多写入越慢。
3. 归档如何验证正确性?——行数 + 校验和比对,先验从库再删主库。

第三部分:面试者参考回答


**【一句话结论】** 归档是把冷数据搬出热库,但必须做到业务无感、数据可查、过程可回滚。
**【三点展开】** 第一,不再访问的数据走分区表导出到对象存储做冷备,秒级完成。第二,还要查的走分批迁移到归档表或 ES,用 DataX 或定时任务,避开业务高峰。第三,迁移工具要能控速,按主键小批量、限制事务大小、检测到主从延迟就暂停。
**【一句边界】** 归档不是删数据,合规场景要保留可恢复路径;如果历史查询很频繁,说明该做冷热分层而不是简单归档。

#4 一个表有用户和时间两个列,现有3个需求:根据用户查;根据日期查;根据日期和用户查;问怎么建立索引? medium practice

题目

一个表有用户和时间两个列,现有3个需求:根据用户查;根据日期查;根据日期和用户查;问怎么建立索引?

我的回答

创建一个联合索引加一个单独索引。联合索引使用用户 ID 在前、时间在后,再单独建立一个时间索引。因为只有联合索引不能覆盖全部情况,建两个单独索引又无法优化日期和用户同时查询,所以选择一个联合索引加一个单列索引,单列索引选时间,因为时间区分度较好,表数据量大时还可以做分区表。

点评

优点:`(user_id, date)` + `date` 的组合完全正确,并想到区分度与分区。
不足:没说透「为什么等值列在前、范围列在后」,也没提索引数量带来的写放大。
补充:最左前缀决定 `(user, date)` 能服务「按用户」和「用户+日期范围」;`(date, user)` 则服务不了只按用户查。用 `EXPLAIN` 验证,按实际查询频次决定是否值得多一个索引。

完整讲解

第一部分:知识重点剖析


这是最左前缀的经典题。三个查询分别是:`where user_id=?`、`where date between ?`、`where user_id=? and date between ?`。`(user_id, date)` 覆盖后两者(等值 + 范围),`date` 单列覆盖纯日期查询。索引设计的判断依据是「等值列在前、范围列在后」,再加上查询频次与写入成本的权衡。

第二部分:考察方向与延伸递进


- 深度:范围列之后的列为什么用不上索引(只能过滤不能定位)
- 广度:索引下推、覆盖索引、索引合并
- 应用:慢查询上线后如何验证索引真的被用上
1. 如果查询是 `date=? and user_id=?` 呢?——等值顺序无关,优化器会重排。
2. 能不能只建 `(user_id, date)` 不建 `date`?——纯日期查询会全表扫,取决于该查询频次。
3. 区分度低的时间列单独建索引有意义吗?——按天范围查有意义,按单点查可能不如全表。

第三部分:面试者参考回答


**【一句话结论】** 建一个 `(user_id, date)` 联合索引加一个 `date` 单列索引,分别覆盖组合查询和纯日期查询。
**【三点展开】** 第一,联合索引把等值的 user_id 放前面、范围的 date 放后面,这样按用户查和用户加日期查都能走。第二,纯日期查询用不上联合索引的最左列,所以要单独建 date 索引。第三,索引不是越多越好,多一个索引就多一份写入和存储成本,要按查询频次取舍。
**【一句边界】** 如果日期查询只查最近几天,可以用分区裁剪替代单列索引;如果写入压力极大,宁可让低频查询走从库。

#5 索引失效的问题是如何排查的,有哪些种情况? medium practice

题目

索引失效的问题是如何排查的,有哪些种情况?

我的回答

索引失效先看对应 SQL 的执行计划,再查看表创建情况,确认表中是否有预期索引。如果有索引,就看执行计划是统计信息导致的失效,还是 SQL 写法有问题。检查查询值和索引字段是否存在隐式转换,比如字符串字段用数字查询;是否没遵循最左前缀、跳过了索引列;是否索引区分度不高,导致要取大量数据、回表后不如全表扫;是否 like 的百分号放在最前面;是否在索引字段上用了函数。

点评

优点:排查顺序正确(先 EXPLAIN、再确认索引存在、再分统计问题与写法问题),主要失效场景基本齐。
不足:漏了 OR 两侧不一致、`!=`/`NOT IN`、`ORDER BY` 与索引顺序不匹配、统计信息过期、回表成本高于全表。
补充:`EXPLAIN ANALYZE` 看真实行数偏差;`SHOW WARNINGS` 看改写;`ANALYZE TABLE` 修统计;覆盖索引可消除回表。

完整讲解

第一部分:知识重点剖析


索引失效分两类:一类是「物理上用不了」,如列上函数、隐式转换、前导百分号、最左前缀断裂;另一类是「优化器算完成本不想用」,如区分度低、回表太多、统计信息过期。排查必须先用 `EXPLAIN` 看 `type/possible_keys/key/rows/Extra`,再判断属于哪一类,而不是背口诀。

第二部分:考察方向与延伸递进


- 深度:优化器成本模型(IO + CPU)如何决定放弃索引
- 广度:索引下推、覆盖索引、索引合并
- 应用:线上 SQL 突然不走索引,先查什么
1. 为什么字符串列传数字会失效?——隐式转换相当于对列做函数,索引有序性被破坏。
2. 统计信息为什么会过期?——采样更新不及时,大范围增删改后需 `ANALYZE TABLE`。
3. `OR` 一定失效吗?——两侧都有索引可能走 index merge,混合无索引列则退化为全表。

第三部分:面试者参考回答


**【一句话结论】** 索引失效先看执行计划,再分「写法用不了」和「优化器不想用」两类定位。
**【三点展开】** 第一,用 EXPLAIN 确认预期索引是否存在、实际走了哪个,必要时 EXPLAIN ANALYZE 看真实行数。第二,写法类问题:列上函数、隐式类型转换、like 前导百分号、违反最左前缀、OR 混合无索引列。第三,成本类问题:区分度太低、回表太多、统计信息过期,可以用覆盖索引或 ANALYZE TABLE 解决。
**【一句边界】** FORCE INDEX 只能兜底,不能治本;真正要改的是索引设计或 SQL 写法。

#6 你是如何进行SQL调优的? medium practice

题目

你是如何进行SQL调优的?

我的回答

我进行 SQL 优化主要看业务情况。一方面根据实际业务确认要返回的结果,尽量不用 * 返回全部列,因为有时只返回很少内容,可以通过覆盖索引;给业务查询字段创建索引,给排序字段创建索引;根据业务情况构建索引,尽可能合并成联合索引;同时控制索引数量,防止过多索引导致写入缓慢和存储压力。

点评

优点:少查列→覆盖索引、过滤/排序建索引、合并联合索引、控制索引数量,四个点都对。
不足:缺方法论主线——定位、分析、改写、结构、验证;排序字段应作为联合索引最后一段,而不是单独建。
补充:用「优化前后 rows_examined / RT」量化;深分页改游标;子查询转 JOIN;OR 转 UNION。

完整讲解

第一部分:知识重点剖析


SQL 调优是闭环而不是技巧集合:**定位**(慢日志、`performance_schema`)→ **分析**(`EXPLAIN` 的 type/rows/Extra)→ **改写**(去函数、去前导百分号、深分页改游标、子查询转 JOIN)→ **结构**(字段类型、冗余、分表、归档)→ **验证**(压测 + 回归 + 前后指标对比)。

第二部分:考察方向与延伸递进


- 深度:`Using filesort` 与 `Using temporary` 的成因与消除
- 广度:SQL 层 → 索引层 → 表结构 → 架构层(缓存/读写分离/分片)
- 应用:不能让 DBA 改索引时,应用侧怎么先优化
1. 深分页为什么慢?——`limit 1000000,10` 要扫描并丢弃前 100 万行,改游标或延迟关联。
2. 排序字段怎么建索引?——放进联合索引末尾,让过滤 + 排序一次完成。
3. 调优后怎么证明有效?——对比执行计划与 `rows_examined`、P99 RT、DB CPU。

第三部分:面试者参考回答


**【一句话结论】** SQL 调优是按「定位、分析、改写、结构、验证」走闭环,不是堆索引。
**【三点展开】** 第一,先用慢日志和 EXPLAIN 定位是哪条 SQL、慢在扫描还是排序。第二,改写优先:少查列走覆盖索引,去掉列上函数和隐式转换,深分页改游标。第三,索引层面把等值列和排序列合进联合索引,同时控制数量避免写放大。
**【一句边界】** 当单表数据量和 QPS 都到瓶颈,SQL 层优化收益有限,要往上走到缓存、读写分离或分片。

#7 MySQL热点数据更新会带来哪些问题? medium practice

题目

MySQL热点数据更新会带来哪些问题?

我的回答

MySQL 热点数据的更新会导致 page cache 更新,影响缓存命中,直接导致数据查询效率降低。针对热点数据要在外侧做好缓存,更新后看业务能否接受短暂不一致,通过缓存降低热点数据访问到数据库的压力。

点评

不足:只覆盖了 Buffer Pool 脏页这一环,且不是主要矛盾;问题链严重不完整。
补充:热点行更新的完整链路是——同一行行锁排队(等锁/死锁/连接堆积)→ undo 版本链膨胀(快照读变慢、purge 压力)→ redo/binlog 写放大、脏页刷盘抖动 → 主从延迟(行锁串行 apply)→ 索引页 latch 竞争。

完整讲解

> 最后更新:2026-10-23 · 基于第 1 次作答

### L1 · 30 秒速记
> 结论:写热点的本质是同一行/同一页被迫串行,缓存只能挡读,救不了写。
> 关键词:行锁排队 · undo 版本链 · redo 写放大 · 主从延迟
> 边界:强一致且无法拆键时,只能单键单写者排队,不能靠异步聚合硬扛。

### L2 · 核心讲解

**核心考点定位**:考察能否把“热点更新”从单点故障追到事务、日志、复制和页面层的连锁反应,并区分读热点与写热点的不同解法;面试官想排除“加缓存/换 Buffer Pool 就能解决写热点”的错误认知。

**机制链(≥3 环)**:
1. 同一行被高频更新 → InnoDB 对聚簇索引记录加 X 锁:后续事务必须排队,等锁超过 `innodb_lock_wait_timeout=50` 秒会报 1205,连接和业务线程被占住。
2. 事务持锁时间被放大 → 每次更新生成 undo 版本:长事务或 purge 跟不上时,版本链越拉越长,快照读要沿 `DB_ROLL_PTR` 回溯,`History list length` 上升。
3. 每次提交都要写 redo 和 binlog → 热点行产生大量小事务:`innodb_flush_log_at_trx_commit=1` 与 `sync_binlog=1` 让每次提交都等刷盘,组提交被高频小事务切碎,出现写放大和刷盘抖动。
4. 同一行的 binlog 在从库按事务回放 → 热点事务在从库仍要竞争同一行:即使开启并行复制,冲突事务也只能串行 apply,主从延迟被放大。
5. 同一索引页被并发修改 → B+ 树页面 latch 竞争和页内记录重排增加:行锁之外还有 page latch 与脏页刷盘竞争,进一步推高尾延迟。

**证据锚点**(去重 ≥10):
- `innodb_lock_wait_timeout=50`:InnoDB 行锁等待上限(秒),超时报 `ERROR 1205`;按业务缩短到 1–5 秒可快速失败。
- `performance_schema.data_lock_waits` / `data_locks`:8.0 查看阻塞关系与持锁事务,配合 `INFORMATION_SCHEMA.INNODB_TRX` 定位长事务。
- `SHOW ENGINE INNODB STATUS`:看 `LATEST DETECTED DEADLOCK`、`History list length`、`Pending writes`,判断锁与 purge 压力。
- `innodb_undo_log_truncate=ON`:8.0 默认开启,undo 表空间可截断;但长事务会阻止 truncate。
- `innodb_purge_threads=4`:8.0 默认 purge 线程数;热点写持续时需观察 purge lag,而不是只调大线程。
- `innodb_max_dirty_pages_pct=90`:脏页比例上限;接近上限时刷盘更激进,可能抢占业务 IO。
- `innodb_io_capacity=200`:8.0 默认 IO 能力基线;SSD 上按实测 IOPS 调整,避免刷脏与业务写互相排队。
- `innodb_flush_log_at_trx_commit=1`:每次提交刷 redo,保证崩溃不丢已提交;降到 2 只省刷盘,不解决行锁。
- `sync_binlog=1`:每次提交同步 binlog;与 `binlog_group_commit_sync_delay`(默认 0 微秒)共同决定组提交等待。
- `binlog_format=ROW`:8.0 默认行格式;热点行更新在 binlog 中按行记录,写放大量与更新行数相关。
- `rpl_semi_sync_master_wait_point=AFTER_SYNC`:半同步在引擎提交后、返回客户端前等从库 ACK;能降低切换丢已提交的风险,但不提升热点写吞吐。
- `max_connections=151`:8.0 默认连接上限;连接池打满时应用线程阻塞在 `getConnection()`,会先拖垮业务线程池。

**对比与权衡**:
| 方案 | 一致性 | 吞吐/并发 | 失败模式 | 适用边界 |
|---|---|---|---|---|
| 直接更新同一行 | 强一致 | 最低,行锁串行 | 等锁超时、连接堆积、主从延迟 | 低频写、强一致且无法拆键 |
| Redis 预扣 + 流水 + MQ 聚合落库 | 最终一致 | 最高 | 丢消息、重复扣、对账复杂 | 可接受短暂不一致,必须有幂等与对账 |
| 按业务键分桶/分片 | 单桶强一致 | 中高 | 余缺调拨、跨桶查询聚合 | 热点键可拆分,查询能接受聚合 |
| 同键单写者队列 | 单键强一致 | 取决于单写者 | 队列积压、崩溃恢复重放 | 账户/库存等必须强一致的场景 |

**常见误区**:
1. 错误:加 Redis 缓存就能解决写热点。→ 正确:缓存只挡读,写仍然落到同一行;要拆写路径或排队。→ 为什么:缓存失效/回填仍要访问数据库,写请求不会因为缓存而并行。
2. 错误:热点更新只是 Buffer Pool 脏页问题。→ 正确:主因是行锁串行,undo/redo 与复制放大是后续成本。→ 为什么:脏页刷盘是结果之一,不是事务阻塞的起点。
3. 错误:分库分表后热点自然消失。→ 正确:如果分片键仍是同一个热键,请求还是落到同一分片/同一行。→ 为什么:路由只改变数据位置,不改变同一键的串行性。
4. 错误:异步扣减天然安全。→ 正确:没有流水、幂等键和对账,异步只会把超卖藏得更深。→ 为什么:重试、重复消费和消息丢失都需要补偿机制。

**适用边界**:如果业务要求强一致且热点键不可拆(如单账户余额),不要用“最终一致 + 异步聚合”硬扛;应选同键单写者或分桶,并准备队列积压、崩溃重放和人工冲正。若热点是读热点,优先缓存/副本,不要误用写拆分。

### 延伸追问(≥3)

1. **为什么热点行更新会让普通查询也变慢?**
- 要点:undo 版本链变长,快照读要沿版本链找可见版本;purge 跟不上时 `History list length` 持续上升。
- 追问落点:长事务如何阻止 purge?如何用 `INNODB_TRX` 找长事务?

2. **加缓存为什么救不了写热点?**
- 要点:缓存挡读不挡写;写仍要走行锁、redo/binlog 和复制。
- 追问落点:缓存失效瞬间的击穿会不会把写热点变成读热点?

3. **分桶/分片后如何保证不超卖?**
- 要点:单桶内用条件更新 `WHERE balance >= ?` 保证原子性;跨桶查询聚合,余缺调拨要有流水。
- 追问落点:桶间调拨失败如何补偿?如何做对账?

4. **主从延迟和热点写是什么关系?**
- 要点:从库回放热点事务仍要竞争同一行,冲突事务无法并行;大事务会进一步放大延迟。
- 追问落点:并行复制为什么不能完全解决同键冲突?

### 面试者参考回答(400–700 字)

**【结论】** 写热点的本质不是缓存命中率,而是同一行或同一页被迫串行;缓存只能挡读,救不了写,必须拆写路径或排队。

**【三点】** 第一,最直接的是行锁排队:同一行 X 锁串行,等锁超过 `innodb_lock_wait_timeout` 会报 1205,连接池和业务线程被占住,严重时形成级联雪崩。第二,隐性成本是 undo/redo:每次更新生成 undo 版本,长事务或 purge 跟不上会让版本链变长、快照读变慢;每次提交又要写 redo 和 binlog,高频小事务把组提交切碎,出现写放大和刷盘抖动。第三,复制和页面层会放大:从库回放同一行事务仍要串行,主从延迟升高;同一索引页的 latch 竞争和脏页刷盘也会推高尾延迟。解法上,能拆键就分桶/分片,能接受最终一致就用 Redis 预扣 + 流水 + MQ 聚合落库,必须强一致就用同键单写者排队;缓存只负责挡读。

**【边界】** 如果业务不能接受短暂不一致,就不要用异步聚合;如果热点键不可拆且要求强一致,只能单写者排队,并接受吞吐上限。任何最终一致方案都要有幂等键、流水和对账,否则不叫一致性方案。

#8 热点账户余额扣减单行QPS上万,如何在保证不超卖的前提下提升并发? high practice

题目

热点账户余额扣减单行QPS上万,如何在保证不超卖的前提下提升并发?

我的回答

单行 QPS 上万代表单个账户更新频繁,可以借鉴组提交等批处理方案:前置缓存更新,顺序创建订单流水,异步发消息到 MQ,再批量更新数据库。方案 1 用 Redis 存临时库存,更新后记录流水,直接更新 MySQL 会因行锁排队导致吞吐下降,所以转批量更新,比如聚合一秒数据更新一次;前面有缓存保证不超卖,中间有流水保证持久化用于对账,最后用 MQ 和批量操作缓解数据库压力。方案 2 是单行 QPS 上万不会来自一个大区,可以按大区划分库存分别扣减,缓解单行压力,但会出现余缺不均,仍用数据库事务保证一致性,对账内容比方案 1 少。

点评

优点:两套方案方向都成立,且主动补了缓存防超卖、流水持久化、对账兜底三段闭环。
不足:题目要三套,缺「同账户单写者 / 余额分桶」;关键正确性没交代——Lua 原子扣减 + requestId 幂等、先落消息表再扣缓存、MQ 幂等消费与死信;批量 SQL 必须带 `balance>=?` 条件。
补充:对账要有差异处理(长款/短款挂账、冲正、超时未支付库存回收),并给出三方案的一致性/吞吐/对账成本对比。

完整讲解

第一部分:知识重点剖析


热点账户扣减是「正确性 + 并发度」的双目标问题。三套主流解法:① Redis 预扣 + 流水 + MQ 聚合批量落库(最终一致,吞吐最高);② 库存/余额分桶或按大区分片(一致性靠 DB 事务,需处理余缺调拨);③ 同账户单写者串行化或余额分桶求和(无跨行锁竞争,吞吐取决于单写者)。无论哪套,防超卖都依赖原子扣减条件与幂等键。

第二部分:考察方向与延伸递进


- 深度:Redis Lua 原子性、消息表与缓存的写入顺序、幂等与悬挂
- 广度:最终一致 → 对账 → 冲正 → 资金安全审计
- 应用:营销余额、库存扣减、积分账户
1. 先扣 Redis 还是先落流水?——先落本地消息表并标记,再扣 Redis,避免扣了钱没凭证。
2. MQ 重复消费怎么防?——业务唯一键 + 状态机,扣减幂等。
3. 缓存挂了怎么办?——降级为 DB 条件更新 + 限流,绝不绕过余额校验。

第三部分:面试者参考回答


**【一句话结论】** 热点账户不能靠单行硬扛,要用「原子预扣 + 流水 + 批量落库」或「拆分 + 排队」把串行变成可控。
**【三点展开】** 第一,Redis 侧用 Lua 原子扣减加请求号幂等,先落消息表再扣缓存,保证不超卖也不丢凭证。第二,MQ 聚合同账户一秒内的扣减,批量执行带 `balance>=?` 的条件更新,落库仍然幂等。第三,如果要求强一致,就按账户或大区拆分库存,用数据库事务扣减,再处理余缺调拨。
**【一句边界】** 异步方案只保证最终一致,必须配流水对账和差异冲正;资金类业务不能接受无对账的最终一致。

#9 MySQL 里有 2000W 数据,Redis 中只存 20W 的数据,如何保证 Redis 中的数据都是热点数据? high practice

题目

MySQL 里有 2000W 数据,Redis 中只存 20W 的数据,如何保证 Redis 中的数据都是热点数据?

我的回答

保证 Redis 存的是热数据,先要判定什么是热数据,有两种方案。一是预先判定,根据历史数据分析和即将推广的活动推断可能的热数据并提前缓存。二是根据实际访问留存,用 LFU 做缓存回收控制热数据留存。淘汰可以按评估周期提前设置,或设置较短的 TTL 保证淘汰比例。热数据被淘汰后回源时,单线程加锁访问数据库,确保只有一个线程回源,不会并发大量线程压垮数据库。缓存穿透方面,基础方案是用布隆过滤器过滤不存在的异常访问;非大型项目一般直接缓存非正常数据并告警,如果某账户或 IP 有大量异常访问,就做封禁或直接返回非法。

点评

优点:结构完整——先定义热、两套判定、淘汰、回填加锁、防穿透,四段闭环,是今天最好的一题。
不足:Redis 侧策略参数没落到具体配置;预测法缺反馈闭环;布隆不支持删除;漏了缓存雪崩(TTL 抖动)与降级限流。
补充:`maxmemory-policy allkeys-lfu` + `lfu-log-factor` / `lfu-decay-time`;容量按热点集 × value 大小 × 1.5 估;低峰预热 + 按实际 QPS 校准热门池。

完整讲解

第一部分:知识重点剖析


「保证 Redis 里都是热点」是伪命题,真实目标是「用有限内存装下大部分访问」。三层:预测层(历史访问、活动计划、离线 TopN 预热点)→ 运行层(LFU/LRU 淘汰 + TTL,容量与命中率监控)→ 兜底层(回源 single-flight、防穿透、防雪崩、降级限流)。

第二部分:考察方向与延伸递进


- 深度:Redis LFU 的 8bit 概率计数 + 时间衰减
- 广度:本地缓存 + 分布式缓存两级、热点 key 探测
- 应用:大促前预热、活动结束后的内存回收
1. 布隆过滤器为什么不能删数据?——位数组共享,删除会影响其他元素,需计数布隆或重建。
2. 热点 key 怎么发现?——代理采样、`hotkeys`、客户端埋点统计。
3. TTL 设太短会怎样?——频繁回源甚至雪崩,必须加随机抖动。

第三部分:面试者参考回答


**【一句话结论】** 热点是动态的,方案必须是「预测 + 实时淘汰 + 回源保护」三层,而不是一次性灌 20 万。
**【三点展开】** 第一,预测层用历史访问和活动计划提前预热,运行层用 allkeys-lfu 加 TTL 让真实热点自然留下。第二,回源时用单飞锁,只放一个线程查库,其他线程等结果,避免击穿。第三,防穿透用布隆加空值缓存,防雪崩用 TTL 抖动和限流降级。
**【一句边界】** 如果热点集本身波动极大,20 万容量可能永远命中不了长尾,要接受一定回源率并保证回源路径可控。

#10 MySQL主从切换瞬间如何避免丢binlog与旧主双写?请设计可回滚的切换方案? high practice

题目

MySQL主从切换瞬间如何避免丢binlog与旧主双写?请设计可回滚的切换方案?

我的回答

主从切换先要确认什么是丢 binlog 和旧主双写。丢 binlog 一般是新主已对外提供写入,但旧主还没提交滞留的 binlog,这部分就丢了;旧主双写是新主已提供服务,旧主还有写入请求,这里也会丢 binlog。核心是确保切换后恢复服务的时间,丢 binlog 要确保旧主同步完所有 binlog,但切换通常发生在旧主已经有问题时,一直等会扩大不可用,所以要划边界,通过重试、心跳确认旧主状态,并把切换期间产生的 binlog 全量转移;如果旧主已经不能服务,就放弃这部分 binlog。旧主双写则要确保切换前旧主不再提供写,双方通过协议关闭写入后再切换。

点评

优点:正确拆出两种数据风险,并提出「先切断旧主写入再切换」的 fencing 方向。
不足:缺具体机制——半同步 `AFTER_SYNC`、GTID 集合判定位点、撤 VIP/read_only/KILL/STONITH 四层隔离;可回滚设计缺失(反向复制、回切路径、checksum 校验);旧主彻底挂掉后的补偿方案与可接受丢失窗口没提。
补充:优先 MHA/Orchestrator 自动切换 + 人工确认,或用 Group Replication 让共识层选主,减少脑裂。

完整讲解

第一部分:知识重点剖析


切换安全的本质是「不产生两个可写主节点 + 不丢已向客户端承诺的数据」。丢数据有两种:旧主未同步的 binlog 未转移;旧主仍可写造成分叉。防丢靠半同步 `AFTER_SYNC`(保证客户端提交成功时至少一个从库收到)+ GTID 对齐;防双写靠 fencing:撤 VIP、`set read_only=1`、`KILL` 残留会话、必要时 STONITH 关机。

第二部分:考察方向与延伸递进


- 深度:`AFTER_SYNC` 与 `AFTER_COMMIT` 的丢失/幻读差异
- 广度:MHA / Orchestrator / Group Replication 的选主差异
- 应用:机房级故障、网络分区下的脑裂防护
1. 为什么 `AFTER_COMMIT` 可能丢数据?——主库先提交再等 ACK,崩溃时从库可能没收到。
2. 切换后如何回滚?——旧主降为只读副本、建立反向复制,校验通过再切回。
3. 怎么验证切换后数据一致?——GTID 集合比对 + `pt-table-checksum`。

第三部分:面试者参考回答


**【一句话结论】** 切换安全靠两件事:用半同步和 GTID 保证不丢已提交数据,用四层 fencing 保证旧主绝不能再写。
**【三点展开】** 第一,开启 `AFTER_SYNC` 半同步,客户端提交成功时至少一个从库已收到,切换时按 GTID 集合对齐,缺的部分用中继日志补偿。第二,切换前先隔离旧主:撤 VIP、设 read_only、杀会话,必要时直接关机,宁停勿双写。第三,保留旧主做反向复制,新主异常可回切,切换后跑 checksum 校验并配合业务对账。
**【一句边界】** 如果旧主彻底损坏,要明确「可接受丢失窗口」并写进 SLA,剩下的差异只能靠业务对账冲正,不能假装零丢失。

📚 查看全部面试记录