补-16
将 ER 图转换为关系模式(确定表结构、主键与外键)属于数据库设计的( )阶段。 A. 逻辑结构设计 B. 概念结构设计 C. 物理结构设计 D. 需求分析
答案:A
📌 考点定位: 数据库设计阶段划分,对应国网考纲第 15 条;牛客笔试选择题数据库知识点。
【结论】 把 ER 图转换为关系模式(定表结构、主键、外键)是逻辑结构设计的标志性任务。选 A。
【逐项辨析】
- A 逻辑结构设计:正确。 该阶段就是把概念模型(ER 图)翻译成 DBMS 支持的关系模式,并做规范化与视图设计。
- B 概念结构设计:错。错在「概念」——概念设计的产出是 ER 图本身,与具体 DBMS 无关,还没走到「定表」这一步。
- C 物理结构设计:错。错在「物理」——物理设计关注索引、存储结构、分区、存取路径,即「表已经定了,再决定它在磁盘上怎么放」。
- D 需求分析:错。错在「需求」——需求分析的产出是数据流图、数据字典、需求说明书,此时还没有任何数据模型。
【知识点】 数据库设计的五个阶段(教材通用划分):
| 阶段 | 核心任务 | 产出物 |
|---|---|---|
| 需求分析 | 调查用户需求与业务规则 | 数据流图(DFD)、数据字典、需求说明书 |
| 概念结构设计 | 抽象实体与联系,与 DBMS 无关 | ER 图 |
| 逻辑结构设计 | ER 图 → 关系模式;规范化;定义视图 | 关系模式(表结构、主键、外键)、视图定义 |
| 物理结构设计 | 选存取方法、建索引、定存储与分区 | 索引方案、存储参数、物理文件组织 |
| 实施与运行维护 | 建库、装载数据、调优、备份恢复 | 运行中的数据库系统 |
阶段间的数据流是单向递进的:需求 → 概念模型(ER 图)→ 逻辑模型(关系模式)→ 物理实现。每一步都在「上一阶段的产出」上加工,所以判断阶段只需问一句:这一步的输入和输出分别是什么。
三个「设计」的分界口诀:
- 概念设计:画 ER 图,只关心「有哪些实体、存在什么联系」。
- 逻辑设计:把 ER 图翻译成表,关心主键 / 外键 / 范式。
- 物理设计:给表配索引和存储,关心磁盘与性能。
逻辑设计阶段实际执行的操作(即本题题干所述):实体转表、属性转列、1:n 联系在 n 端加外键、m:n 联系建独立联系表(见补-15),随后做规范化检查。
【知识点扩写·逻辑设计的具体操作清单】
- 实体 → 表;属性 → 列(注意类型与约束)
- 1:1 联系 → 合并或一侧外键
- 1:n 联系 → n 端加外键
- m:n 联系 → 新建联系表,主键为两端键组合
- 规范化检查(至少到 3NF,必要时反范式)
- 定义用户视图(外模式)
- 与应用代码的模型映射(ORM Entity) 以上 1–6 全部属逻辑结构设计;索引与物理参数属物理设计。
【记忆锚点】 「画 ER 图 = 概念,ER 转表 = 逻辑,加索引 = 物理」。
【易混对比】
- 概念设计 vs 逻辑设计:前者产出 ER 图(与 DBMS 无关),后者产出关系模式(依赖 DBMS)。这是本考点最常设的陷阱,分界就是「有没有变成表」。
- 逻辑设计 vs 物理设计:前者决定「有哪些表、表之间怎么关联」,后者决定「数据在磁盘上怎么存、查询走不走索引」。
- 需求分析 vs 概念设计:需求分析产出数据流图 / 数据字典(带业务流程视角),概念设计产出 ER 图(纯数据视角)。
- 换问法:若题目问「确定索引和存储结构属于哪个阶段」,答案是物理结构设计;问「画出 ER 图」,答案是概念结构设计;问「数据字典属于哪个阶段」,答案是需求分析。
【自测】 「根据业务流程画出数据流图与数据字典」属于数据库设计的哪个阶段?「为订单表的 user_id 列建立 B+ 树索引」又属于哪个阶段?
答:需求分析;物理结构设计。与补-15、补-01 连考;国网考纲第 15 条。
【选项误解】
- A 正确项:ER→关系模式(定表、主键、外键)= 逻辑结构设计的标志性任务。
- B 误解来源:概念设计产出是 ER 图本身,还没「变成表」。
- C 误解来源:物理设计管索引/存储/分区,此时表已定。
- D 误解来源:需求分析产出 DFD/数据字典,还没有数据模型。
【知识关联】
- 主库关联:补-15(ER 联系类型→逻辑设计的转换规则);M22/M23(主键选型属逻辑/物理设计交界);M21(慢查询日志——物理调优阶段)。
- 国网/408:国网第 15 条「数据库应用系统设计与开发」;408 可能考设计阶段填空。
- 面试追问:① 规范化属于哪个阶段?→ 逻辑结构设计。② 选 B+ 树索引属于哪阶段?→ 物理结构设计。③ 数据字典在哪阶段形成?→ 需求分析阶段开始建立,后续阶段不断充实。
【拓展延伸】
- 变式问法:①「画 ER 图」→ 概念设计;②「ER 转表、定主键外键」→ 逻辑设计;③「建索引、选存储引擎」→ 物理设计;④「数据流图/数据字典」→ 需求分析。
- 工程参数/命令:
逻辑设计产出:
CREATE TABLE 语句 / 数据模型文档 / 外键关系 / 视图定义
物理设计动作:
ALTER TABLE ... ADD INDEX ...
ENGINE=InnoDB, ROW_FORMAT=DYNAMIC
分区表 PARTITION BY RANGE ...
选择 charset/utf8mb4, collation
运维阶段:
慢查询日志 slow_query_log=ON, long_query_time=1
EXPLAIN / SHOW PROFILE 调优- 阶段漏斗:需求 → 概念(ER)→ 逻辑(关系模式)→ 物理(索引/存储)→ 实施运维。判断阶段只需问:这一步的输入和输出是什么。
补-17
事务特性中,「并发执行的事务之间互不干扰」与「事务一旦提交其修改即永久保存」分别对应( )。 A. 一致性;持久性 B. 原子性;隔离性 C. 隔离性;持久性 D. 持久性;隔离性
答案:C
📌 考点定位: 事务的 ACID 特性,对应国网考纲第 13 条,国企/银行笔试必考。
【结论】 「并发事务互不干扰」是隔离性,「提交后永久保存」是持久性。选 C。
【逐项辨析】
- A 一致性;持久性:错。错在第一空 —— 一致性指「事务前后数据库满足完整性约束」,「互不干扰」说的是并发隔离,不是一致性。
- B 原子性;隔离性:错。两空都不符 —— 原子性是「全做或全不做」,第二空应是「永久保存」对应的持久性。
- C 隔离性;持久性:正确。 两空与 ACID 定义逐字对应,无需任何补充假设。
- D 持久性;隔离性:错。错在两空颠倒 —— 「互不干扰」是隔离性,「永久保存」是持久性。
【知识点】 ACID 四特性的定义与实现机制:
| 特性 | 含义(判定字眼) | 实现机制 |
|---|---|---|
| 原子性 Atomicity | 事务的操作要么全做、要么全不做 | Undo 日志(回滚已做的修改) |
| 一致性 Consistency | 事务执行前后数据库都满足完整性约束 | 由 A、I、D 共同保障(无独立机制) |
| 隔离性 Isolation | 并发事务互不干扰,如同串行执行 | 封锁(锁)+ MVCC |
| 持久性 Durability | 提交后的修改永久生效,故障也不丢 | Redo 日志(重做已提交修改) |
推导「为什么原子性靠 Undo、持久性靠 Redo」:
- 事务执行到一半失败,需要擦掉已经写下的痕迹 → 用前像把数据改回原样 → Undo。
- 事务已提交但脏页还没刷盘时宕机,需要把提交的修改补回去 → 用后像重做 → Redo。
两者共用同一份日志文件(同时记录前像与后像),只是扫描方向与用途相反(见补-20)。
一致性的特殊地位:它是目的,A、I、D 是手段。以转账为例,若隔离性被破坏(脏读)或原子性被破坏(扣钱后未加钱),一致性都会崩。因此考题里「一致性」从不与某一种日志 / 锁绑定,这正是 A 项设陷阱的地方。
隔离性的强弱由隔离级别调节:READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE,级别越高隔离性越强、并发度越低。
【知识点扩写·ACID 与日志锁的映射】
Atomicity ← Undo Log(回滚未完成事务)
Consistency ← 业务约束 + A/I/D 共同结果
Isolation ← 锁(S/X/意向/间隙)+ MVCC(ReadView)
Durability ← Redo Log(WAL)+ 刷盘策略
崩溃恢复:
已提交未刷盘 → Redo
未提交 → Undo
两阶段提交(MySQL):
prepare(redo) → 写 binlog → commit(redo)
任一步失败可按 binlog 是否存在决定提交或回滚对应主库:M13 redo、M14 binlog、M15 undo、M16 两阶段提交。
【记忆锚点】 「原子靠 Undo,持久靠 Redo,隔离靠锁」——只剩一致性不绑具体机制,它由另外三个共同兜底。
【易混对比】
- 原子性 vs 一致性:原子性是「全做或全不做」(操作层面),一致性是「数据满足完整性约束」(状态层面)。
- 隔离性 vs 持久性:隔离性管并发(多个事务之间互不干扰),持久性管故障(宕机 / 断电后不丢)。
- 隔离性 vs 隔离级别:隔离性是特性,隔离级别是「放宽隔离性以换取并发」的档位。
- 换问法:若题目问「「事务中的操作要么全做要么全不做」对应哪个特性」,答案换成原子性;再问「靠什么实现」,答案换成 Undo 日志。
【自测】 某事务提交后数据库立刻断电,重启后该事务的修改依然存在。这体现了哪个特性?由什么机制保证?
答:持久性;由 Redo 日志保证(把已提交但未落盘的后像重新写入)。与补-18、补-20 连考;国网考纲第 13 条。
【选项误解】
- A 误解来源:第一空把「互不干扰」当成一致性。一致性是事务前后满足完整性约束,是目的;互不干扰说的是并发隔离。
- B 误解来源:两空都错——原子性是「全做或全不做」,第二空应是持久性。
- C 正确项:互不干扰=隔离性;提交后永久=持久性。
- D 误解来源:两空对调,最常见于记住了四个词但没绑定义。
【知识关联】
- 主库关联:M09(默认隔离级别 RR)、M10(幻读)、M11(RR 防幻读机制)、M12(MVCC)、M13(redo log)、M15(undo log)、M16(redo+binlog 一致性)、M32(RC 能避免什么)。ACID 是这一整片题的理论总纲。
- 国网/408:国网第 13 条「事务处理和并发控制」;408 必考 ACID 定义与实现机制对应表。
- 面试追问:① 一致性靠什么保证?→ 不靠单一机制,由 A、I、D 以及业务约束共同保障。② 隔离性实现?→ 锁 + MVCC。③ 原子性/持久性日志?→ Undo / Redo(WAL)。④ MySQL 如何保证提交后不丢?→ redo log 二阶段提交 + binlog。
【拓展延伸】
- 变式问法:①「全做或全不做」→ 原子性 + Undo;②「并发互不干扰」→ 隔离性 + 锁/MVCC;③「提交后宕机不丢」→ 持久性 + Redo;④「转账前后总额不变」→ 一致性(由其他特性共同保障)。
- 工程参数/命令:
-- 查看/设置隔离级别(8.0)
SELECT @@transaction_isolation;
SET SESSION transaction_isolation='READ-COMMITTED';
-- redo/binlog
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; -- 默认1
SHOW VARIABLES LIKE 'sync_binlog'; -- 默认1(8.0)
-- 双1配置:每次提交 redo 与 binlog 都 fsync,最安全、性能略降- 持久化代价:
innodb_flush_log_at_trx_commit=0/2可提升 TPS,但宕机可能丢最近 1 秒事务——金融库必须保持 1。
补-18
关于两段锁协议(2PL),下列说法正确的是( )。 A. 遵循 2PL 的并发调度一定是无死锁的 B. 2PL 要求事务分为「扩展阶段(只能加锁、不能解锁)」与「收缩阶段(只能解锁、不能加锁)」,它是并发调度可串行化的充分条件 C. 2PL 要求事务在开始时一次性申请全部所需锁 D. 2PL 与事务的隔离级别无关
答案:B
📌 考点定位: 并发控制——两段锁协议,对应国网考纲第 13 条,国企笔试高频。
【结论】 2PL = 加锁全部在解锁之前完成(扩展期只加锁、收缩期只解锁),是调度可串行化的充分条件,但不保证无死锁。选 B。
【逐项辨析】
- A 遵循 2PL 的调度一定无死锁:错。错在「一定无死锁」——2PL 只保证可串行化,与死锁无关。两事务若加锁顺序相反(T₁ 先 x 后 y、T₂ 先 y 后 x)就形成循环等待,照样死锁。
- B 分扩展阶段与收缩阶段,是可串行化的充分条件:正确。 前半句是 2PL 的定义,后半句是 2PL 的标准结论,两项都准确。
- C 要求开始时一次性申请全部锁:错。错在「一次性申请全部锁」——这描述的是一次封锁法(保守 2PL),是另一种并发控制策略,不是「两段锁」的本义。
- D 2PL 与隔离级别无关:错。错在「无关」——2PL 正是实现隔离性、进而支撑各隔离级别的技术基础。
【知识点】 两段锁协议(Two-Phase Locking,2PL)的定义:
- 扩展阶段(growing phase):只能申请锁(S 或 X),不能释放任何锁。
- 收缩阶段(shrinking phase):只能释放锁,不能再申请任何锁。
- 一旦释放了第一把锁,就进入收缩阶段,此后不得再加锁。
三条结论必须分开记:
| 命题 | 是否成立 | 说明 |
|---|---|---|
| 2PL ⇒ 可串行化 | ✅ | 充分条件 |
| 可串行化 ⇒ 2PL | ❌ | 非必要条件,存在可串行化但非 2PL 的调度 |
| 2PL ⇒ 无死锁 | ❌ | 2PL 完全不解决死锁 |
| 2PL ⇒ 无级联回滚 | ❌ | 需严格 2PL(S2PL)才行 |
2PL 的主要变体(易考的延伸):
| 协议 | 额外要求 | 解决的问题 | 代价 |
|---|---|---|---|
| 保守 2PL(一次封锁法) | 事务开始前一次申请全部锁 | 可避免死锁 | 并发度低,需预知全部锁 |
| 顺序封锁法 | 所有事务按同一顺序申请锁 | 可避免死锁 | 需事先规定顺序 |
| 严格 2PL(S2PL) | 所有 X 锁持有到事务结束才释放 | 避免级联回滚,保证可恢复 | 锁持有时间长 |
| 强严格 2PL | 所有锁(含 S 锁)都持有到事务结束 | 调度严格可串行化 | 并发度进一步降低 |
死锁的四个处理方向:预防(一次封锁法、顺序封锁法)、检测(构造等待图,检查有无回路)、解除(选代价最小的事务回滚)。InnoDB 中另有 innodb_deadlock_detect 开关(开启时主动检测并回滚)与 innodb_lock_wait_timeout 超时(关闭检测时的兜底)两条路径。
【知识点扩写·2PL 与变体对比】
| 协议 | 规则 | 可串行化 | 无死锁 | 无级联回滚 |
|---|---|---|---|---|
| 基本 2PL | 先全加锁后全解锁 | ✓充分 | ✗ | ✗ |
| 保守 2PL | 开始一次申请全部锁 | ✓ | ✓(预声明) | 看版本 |
| 顺序封锁 | 按全局顺序申请 | ✓ | ✓ | — |
| 严格 2PL | X 锁持到结束 | ✓ | ✗ | ✓ |
| 强严格 2PL | 所有锁持到结束 | 严格可串行 | ✗ | ✓ |
InnoDB 实践:写锁持到提交 + RR 下间隙锁 → 更接近严格 2PL + 扩展封锁。
【记忆锚点】 「2PL 保可串行化,不保无死锁」——两句话必须分开记,考题十有八九就在这两句上设陷阱。
【易混对比】
- 2PL vs 一次封锁法:2PL 允许在扩展期逐把加锁;一次封锁法要求开始时一次加完。前者不防死锁,后者可防。
- 充分条件 vs 必要条件:2PL 是充分条件 —— 满足则必可串行化,但不满足也可能恰好可串行化。
- 2PL vs 严格 2PL:前者不防级联回滚,后者(X 锁持到事务结束)可防。
- 换问法:若题目问「哪种策略能避免死锁」,答案是一次封锁法 / 顺序封锁法,而不是 2PL;问「2PL 保证什么」,答案是可串行化。
【自测】 事务 T₁ 依次对 x、y 加 X 锁,事务 T₂ 依次对 y、x 加 X 锁,两者都严格遵循 2PL。它们会死锁吗?为什么?
答:会死锁。T₁ 持有 x 等 y、T₂ 持有 y 等 x,形成循环等待;2PL 只保证可串行化、不防死锁,需靠死锁检测或顺序封锁来避免。与补-19 连考;国网考纲第 13 条。
【选项误解】
- A 误解来源:把「可串行化」与「无死锁」绑在一起。2PL 只保证可串行化,反序加锁照样死锁。
- B 正确项:扩展期只加锁、收缩期只解锁 + 是可串行化充分条件。
- C 误解来源:描述的是一次封锁法(保守 2PL),不是 2PL 本义。
- D 误解来源:2PL 是实现隔离性的技术基础,与隔离级别强相关。
【知识关联】
- 主库关联:M09–M12(隔离级别/MVCC——隔离性的实现层)、M17(无索引 UPDATE 锁范围)、M18(意向锁)、补-21(乐观/悲观锁)、补-23(InnoDB 死锁检测)。
- 国网/408:国网第 13 条;408 并发控制大题常考 2PL 与冲突可串行化/视图可串行化的关系。
- 面试追问:① 2PL 是可串行化的必要条件吗?→ 不是,只是充分条件,存在可串行但非 2PL 的调度。② 严格 2PL 解决什么?→ 级联回滚,X 锁持到事务结束。③ InnoDB 是严格 2PL 吗?→ 更接近:锁通常持到事务结束(尤其写锁),并配合 MVCC 快照读。
【拓展延伸】
- 变式问法:①「2PL 保证?」→ 可串行化;②「能避免死锁的协议?」→ 一次封锁法/顺序封锁法;③「防止级联回滚?」→ 严格 2PL。
- 完整推导·为何 2PL 可串行化:
设并发调度 S 中各事务均服从 2PL
冲突操作次序由锁的相容性决定
扩展期:事务持续获得锁,其冲突操作必须排在其他持锁事务之后
收缩期:只放锁,不再产生新的「排到更前」的冲突约束
可证:调度的冲突图中无环 ⇔ 可串行化
(教材结论:2PL 是可串行化的充分条件,证明见冲突可串行化定理)
反例说明 2PL⇏无死锁:
T1: lock-x(x) … lock-x(y)
T2: lock-x(y) … lock-x(x)
双方均遵守 2PL,但形成循环等待- 工程参数/命令:
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; -- 默认50s
SHOW VARIABLES LIKE 'innodb_deadlock_detect'; -- 默认ON
SELECT * FROM information_schema.INNODB_LOCKS; -- 5.7
SELECT * FROM performance_schema.data_locks; -- 8.0补-19
为正确实现并发控制,关于共享锁(S 锁)与排他锁(X 锁)的申请规则与相容性,下列说法正确的是( )。 A. 读操作前申请 S 锁、写操作前申请 X 锁;S 锁之间相容,S 与 X、X 与 X 均不相容 B. 读操作前申请 X 锁、写操作前申请 S 锁 C. S 锁与 X 锁之间完全相容,可同时持有 D. 事务只需在结束时统一加锁,过程中不必申请
答案:A
📌 考点定位: 并发控制——封锁类型与相容矩阵,对应国网考纲第 13 条。
【结论】 读操作前申请 S 锁、写操作前申请 X 锁;相容矩阵中只有 S–S 相容,其余三种组合全部互斥。选 A。
【逐项辨析】
- A 读前申请 S、写前申请 X;S–S 相容,S–X 与 X–X 均不相容:正确。 申请规则与相容矩阵两项都准确,无一处需要修正。
- B 读操作前申请 X 锁、写操作前申请 S 锁:错。错在把两种锁的申请规则说反 —— 写操作要求独占,必须申请 X 锁,读操作共享,申请 S 锁。
- C S 锁与 X 锁完全相容、可同时持有:错。错在「完全相容」——事实恰好相反,S 与 X 互斥,否则读事务会读到别的事务尚未提交的修改。
- D 事务只需在结束时统一加锁、过程中不必申请:错。错在「过程中不必申请」——这等于放弃并发控制,无法保证调度的可串行性。
【知识点】 两种基本封锁类型:
| 锁类型 | 别名 | 允许的操作 | 对其他事务的约束 |
|---|---|---|---|
| 共享锁 S | 读锁 | 只读 | 其他事务只能再加 S 锁,不能加 X 锁 |
| 排他锁 X | 写锁 | 读 + 写 | 其他事务既不能加 S 也不能加 X |
相容矩阵(行 = 已持有,列 = 新申请;✅ 相容可立即获准,❌ 互斥需等待):
| 已持有 \ 新申请 | S | X |
|---|---|---|
| S | ✅ | ❌ |
| X | ❌ | ❌ |
结论:四格中只有 S–S 一格相容,其余三格全部互斥。
三条推导(对应矩阵的三格 ❌ 中的关键两格):
- 为什么 S–S 相容?S 锁持有者只读不写,多个读者同时读互不影响,数据不会被改动,因此可以共享。
- 为什么 S–X 必须互斥?若允许,读者可能在写者未提交时读到「脏数据」;一旦写者回滚,读者读到的值从未真实存在过(脏读)。
- 为什么 X–X 必须互斥?两个写者同时改同一数据会导致丢失修改(后写覆盖前写),这正是 X 锁要防的核心问题。
三级封锁协议(封锁协议解决「加什么锁、持有多久」):
- 一级:写操作加 X 锁并持到事务结束 → 防丢失修改。
- 二级:一级 + 读操作加 S 锁、读完即放 → 再防脏读。
- 三级:二级 + S 锁也持到事务结束 → 再防不可重复读。
补充:InnoDB 在 REPEATABLE READ 级别下还用间隙锁(Gap Lock)+ 记录锁 = Next-Key Lock 来防幻读。
【知识点扩写·三级封锁协议】
| 协议 | 规则 | 防止的问题 |
|---|---|---|
| 一级 | 写前加 X,持到事务结束 | 丢失修改 |
| 二级 | 一级 + 读前加 S,读完即放 | 脏读 |
| 三级 | 二级 + S 也持到事务结束 | 不可重复读 |
与 2PL 关系:封锁协议回答「加什么锁、持多久」;2PL 回答「加锁/解锁的时序」。两者正交,常组合使用。 InnoDB 补充:RR 下当前读使用 Next-Key Lock = 记录锁 + 间隙锁,进一步防幻读。
【记忆锚点】 「读加 S、写加 X;只有 S 和 S 能做朋友」。
【易混对比】
- S 锁 vs X 锁:S 是「可共享的读锁」,X 是「独占的写锁」。
- X 锁防丢失修改 vs S 锁防脏读:一级封锁协议(写加 X 持到结束)防丢失修改;二级(读加 S、读完即放)防脏读;三级(S 锁持到事务结束)防不可重复读。
- 封锁协议 vs 两段锁协议:封锁协议解决「加什么锁、持多久」,2PL 解决「何时加、何时放」(见补-18)。两者不是一回事,常被混问。
- 换问法:若题目问「哪两种锁相容」,答案是只有 S 与 S;问「读操作该加什么锁」,答案是 S 锁。
【自测】 事务 T₁ 已对数据项 x 加了 S 锁,此时 T₂ 申请对 x 加 S 锁、T₃ 申请对 x 加 X 锁,两者的申请分别会怎样?
答:T₂ 立即获准(S–S 相容),T₃ 需等待(S–X 互斥)。与补-18 连考;国网考纲第 13 条。
【选项误解】
- A 正确项:读 S 写 X;相容矩阵仅 S–S 相容。
- B 误解来源:S/X 职责说反。写操作需独占,必须 X。
- C 误解来源:以为「都是锁所以能共存」。S 与 X 互斥,否则会脏读。
- D 误解来源:结束时才加锁等于放弃并发控制,无法保证可串行。
【知识关联】
- 主库关联:M17(无索引更新的锁代价)、M18(意向锁 IS/IX 在表级与行级锁的协调)、M32(RC/并发问题)、补-18(2PL 何时加放锁)、补-21(FOR UPDATE=悲观 X 锁)。
- 国网/408:国网第 13 条;408 常考封锁相容矩阵与三级封锁协议。
- 面试追问:① 意向锁是干什么的?→ 表级标记「内部有行锁」,快速判断表锁兼容性,避免逐行检查。② MySQL 的 LOCK IN SHARE MODE / FOR SHARE?→ S 锁;FOR UPDATE → X 锁。③ 三级封锁协议分别防什么?→ 一级防丢失修改,二级再防脏读,三级再防不可重复读。
【拓展延伸】
- 变式问法:①「哪两种锁相容?」→ 仅 S 与 S;②「读操作加?」→ S;③「写操作加?」→ X;④「矩阵题:已持 X,申请 S?」→ 互斥等待。
- 工程参数/命令:
-- 悲观锁读/写
SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE; -- S(5.7)
SELECT * FROM t WHERE id=1 FOR SHARE; -- S(8.0)
SELECT * FROM t WHERE id=1 FOR UPDATE; -- X
-- 查看锁
SHOW ENGINE INNODB STATUS\G -- LATEST DETECTED DEADLOCK
SELECT * FROM performance_schema.data_locks;- 相容矩阵记忆:
申请S 申请X
持有S 允许 等待
持有X 等待 等待补-20
数据库系统使用「日志文件 + 检查点」进行故障恢复,其核心依据是( )。 A. 故障后重建整个数据库即可,无需日志文件 B. 日志只记录已提交事务的修改,未提交事务不必记录 C. 检查点的作用是删除过期日志以节省磁盘空间 D. 利用日志对「已提交但可能未写回磁盘」的事务做 Redo、对「未提交」的事务做 Undo,检查点则用于缩短恢复时需要扫描的日志范围
答案:D
📌 考点定位: 数据库恢复技术——日志与检查点,对应国网考纲第 14 条,国企笔试高频。
【结论】 恢复依据是:对「已提交但可能未写回磁盘」的事务做 Redo、对「未提交」的事务做 Undo,检查点则用于缩短需要扫描的日志范围。选 D。
【逐项辨析】
- A 故障后重建整个数据库即可、无需日志:错。错在「无需日志」——全库重建不现实,且会把已提交的数据一并丢掉,持久性直接失效。
- B 日志只记录已提交事务的修改、未提交事务不必记录:错。错在「未提交不必记录」——未提交事务的修改必须记(记下前像),否则故障后无从 Undo,原子性无从保证。
- C 检查点的作用是删除过期日志以节省磁盘空间:错。错在「删除过期日志」——检查点的作用是刷脏页并记录活跃事务表与日志位置,从而缩短恢复扫描范围;日志清理只是副产品,不是它的设计目的。
- D 已提交做 Redo、未提交做 Undo,检查点缩短扫描范围:正确。 三句话分别对应恢复策略与检查点职责,完全准确。
【知识点】 日志文件(Log)按时间顺序记录的内容:
| 日志记录 | 形式 | 用途 |
|---|---|---|
| 事务开始 | <Tᵢ, START> | 标记事务起点 |
| 更新记录 | <Tᵢ, X, 前像, 后像> | 前像供 Undo、后像供 Redo |
| 提交 / 回滚 | <Tᵢ, COMMIT> / <Tᵢ, ABORT> | 判定该事务该 Redo 还是 Undo |
恢复策略(正向 Redo、反向 Undo):
| 事务状态 | 恢复动作 | 保证的特性 |
|---|---|---|
| 有 BEGIN 也有 COMMIT,但修改可能未落盘 | Redo(用后像重做) | 持久性 |
| 有 BEGIN 但无 COMMIT | Undo(用前像撤销) | 原子性 |
推导「为什么必须记录未提交事务的修改」:Undo 需要前像才能把数据改回原样。若日志里不写未提交事务的修改,宕机后就无从得知它改过什么,只能任由脏数据留在磁盘上 —— 原子性当场失效。这就是 B 项的致命错误。
检查点(Checkpoint)机制:系统定期把内存缓冲区的脏页刷回磁盘,并在日志中写入一条检查点记录,同时保存当前活跃事务表与日志位置。恢复时只需从最近一次检查点开始扫描(该点之前的修改已全部落盘),恢复时间从「扫描全量日志」降为「扫描一个区间」。
补充:检查点之后,检查点之前且已提交的事务日志可以被归档或清理,所以「节省空间」是它的副产品,而非定义 —— 这正是 C 项偷换概念的陷阱。
按故障类型选择恢复手段:
| 故障类型 | 影响范围 | 恢复手段 |
|---|---|---|
| 事务故障 | 单个事务 | 用日志 Undo 该事务 |
| 系统故障(宕机 / 断电) | 内存丢失、磁盘完好 | Redo 已提交 + Undo 未提交 |
| 介质故障(磁盘损坏) | 磁盘数据丢失 | 备份 + 日志联合恢复 |
【知识点扩写·为什么必须记未提交的修改】
假设日志只记已提交事务:
T 在提交前修改了 X(脏页可能已刷盘)
宕机重启后:磁盘上可能是 T 改过的 X
日志中却没有 T 的记录 → 无法得知前像 → 无法 Undo
结果:未提交事务的痕迹残留在数据库中 → 原子性被破坏
因此:日志必须「先记后改」(WAL),未提交的更新也要写入,标记其事务状态
WAL 规则:数据页刷盘前,对应日志必须先落盘对应主库 M13:redo 的 write-ahead 特性。
【记忆锚点】 「已提交 → Redo,未提交 → Undo;检查点只缩短扫描范围,不删日志」。
【易混对比】
- Undo vs Redo:Undo 用前像擦掉未提交的改动;Redo 用后像补回已提交的改动。扫描方向:Redo 正向、Undo 反向。
- 检查点 vs 日志清理:检查点的目的是加速恢复(缩短扫描区间),删日志只是附带效果,不是它的定义。
- 日志 vs 备份:日志是「增量记录」(用于事务 / 系统故障恢复),备份是「全量副本」(用于介质故障,仍需配合日志重做)。
- 换问法:若题目问「Redo 依据日志中的哪部分内容」,答案是后像;问「检查点记录保存了什么」,答案是活跃事务表与日志位置。
【自测】 系统在事务 T₃ 更新数据之后、提交之前宕机,且 T₃ 的修改可能已部分写入磁盘。重启后恢复程序应对 T₃ 执行什么操作?依据日志中的哪部分内容?
答:Undo(撤销);依据日志中的前像,把 T₃ 改过的数据恢复成更新前的值。与补-17、补-18 连考;国网考纲第 14 条。
【选项误解】
- A 误解来源:以为可以「推倒重来」。全库重建会丢已提交数据,持久性失效。
- B 误解来源:以为未提交不必记日志。没有前像就无法 Undo,原子性无从谈起。
- C 误解来源:把检查点的副产品(可归档旧日志)当设计目的。检查点核心是缩短恢复扫描范围。
- D 正确项:已提交→Redo,未提交→Undo,检查点缩短扫描区间。
【知识关联】
- 主库关联:M13(redo log)、M14(binlog)、M15(undo log)、M16(两阶段提交保证 redo/binlog 一致)、补-17(ACID 与日志映射)。
- 国网/408:国网第 14 条「备份和恢复」;408 恢复技术大题(日志/检查点/镜像)高频。
- 面试追问:① 检查点记录了什么?→ 活跃事务表 + 日志位置 + 把脏页刷盘。② Redo 用前像还是后像?→ 后像;Undo 用前像。③ 崩溃恢复扫描方向?→ 从最近检查点正向 Redo 已提交,反向 Undo 未提交。
【拓展延伸】
- 变式问法:①「Redo 依据日志中的?」→ 后像;②「Undo 依据?」→ 前像;③「检查点主要目的?」→ 缩短恢复扫描范围,不是删日志;④「介质故障怎么恢复?」→ 备份 + 日志。
- 完整恢复流程推导:
系统故障(断电、宕机,磁盘完好)恢复:
1. 从 redo log 定位最近检查点
2. 从检查点正向扫描日志,构造:
- REDO-LIST:检查点之后已提交事务
- UNDO-LIST:检查点之后未提交事务
3. 对 REDO-LIST:按后像重做(幂等:可重复执行)
4. 对 UNDO-LIST:按前像反向撤销
5. 写恢复完成标记
事务故障:只需 Undo 该事务
介质故障:装载最近备份 → 再用日志重做到故障前- 工程参数/命令:
SHOW GLOBAL STATUS LIKE 'Innodb_redo_log_checkpoint_lsn'; -- 本机 8.0.46 实测:SHOW VARIABLES LIKE 'innodb_log_checkpoint%' 返回**空集**(含 checkpoint 的系统变量只有 replica/slave_checkpoint_period|group),当前检查点 LSN 要看状态变量 Innodb_redo_log_checkpoint_lsn
SHOW ENGINE INNODB STATUS\G -- LOG 段中的 checkpoint
-- binlog 保留与清理
SHOW BINARY LOGS;
PURGE BINARY LOGS BEFORE '2026-01-01 00:00:00';补-21
关于乐观锁与悲观锁,下列说法正确的是( )。 A. 乐观锁通过数据库的行锁实现,悲观锁通过版本号实现 B. 悲观锁假定冲突频繁,先加锁再操作(如 SELECT ... FOR UPDATE);乐观锁假定冲突较少,不加锁,靠版本号或 CAS 在更新时校验 C. 乐观锁在任何场景下性能都优于悲观锁 D. 两者都必须由数据库内核提供,应用层无法实现
答案:B
📌 考点定位: MySQL 锁——乐观锁与悲观锁,互联网面试高频,主库未覆盖。
【结论】 悲观锁「先锁后改」,乐观锁「先改后校验」——前者假定冲突频繁、操作前加锁,后者假定冲突较少、不加锁而靠版本号或 CAS 在更新时校验。选 B。
【逐项辨析】
- A 乐观锁通过数据库的行锁实现,悲观锁通过版本号实现:错。把两者的实现机制完全说反了 —— 行锁是悲观锁的手段,版本号是乐观锁的手段。
- B 悲观锁假定冲突频繁,先加锁再操作(如
SELECT ... FOR UPDATE);乐观锁假定冲突较少,不加锁,靠版本号或 CAS 在更新时校验:正确。 一句话点全了「冲突假设 + 加锁时机 + 实现手段」三个要素。 - C 乐观锁在任何场景下性能都优于悲观锁:错。错在「任何场景」这个绝对化字眼 —— 高冲突场景下乐观锁的重试与回滚代价很大,可能反而不如悲观锁。
- D 两者都必须由数据库内核提供,应用层无法实现:错。乐观锁本质是应用层的「版本号 + 条件更新 + 重试」约定,不需要数据库内核提供任何锁。
【知识点】 两种锁的分野不是「谁更好」,而是「假设不同」。
悲观锁(Pessimistic Locking):假定并发冲突频繁,操作前先加锁,其他事务只能阻塞等待。由数据库内核提供:
- 排他锁:
SELECT ... FOR UPDATE→ X 锁,可读可写,排斥其他任何锁。 - 共享锁:
SELECT ... LOCK IN SHARE MODE(MySQL 8.0 起写作FOR SHARE)→ S 锁,只读,互相兼容但与 X 锁互斥。
乐观锁(Optimistic Locking):假定冲突较少,读取时不加锁,提交更新时才校验数据是否被改过。
- 版本号法:
UPDATE account SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 3;- 时间戳法:与版本号法同理,只是把
version换成update_time。 - CAS 思想:Java 的
AtomicInteger.compareAndSet、Redis 的WATCH+MULTI都是「比较并交换」的乐观锁实现。
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 冲突假设 | 冲突频繁 | 冲突较少 |
| 加锁时机 | 操作前加锁(先锁后改) | 不加锁,更新时校验(先改后校验) |
| 实现手段 | FOR UPDATE / LOCK IN SHARE MODE(数据库内核) | 版本号 / 时间戳 / CAS(应用层约定) |
| 并发度 | 低(阻塞等待) | 高(不阻塞,冲突时重试) |
| 失败代价 | 等待、死锁、锁超时 | 重试(高冲突时形成重试风暴) |
| 典型场景 | 库存扣减、账户转账(强一致) | 商品详情、配置项修改(读多写少) |
选型判据:冲突概率 × 重试成本 < 加锁等待成本 → 用乐观锁;反之用悲观锁。
【记忆锚点】 「悲观先锁后改,乐观先改后校验」——悲观锁像「排队过闸机」,乐观锁像「改完再对暗号,对不上就重来」。
【易混对比】
- 乐观锁 vs 悲观锁:不是优劣之争,而是「假设之争」;冲突少用乐观、冲突多用悲观。
- 乐观锁 vs 数据库锁:乐观锁不是数据库锁,而是应用层约定;被问「怎么实现」必须答「版本号 + 条件更新 + 重试」。
- 乐观锁 vs MVCC:MVCC 是数据库内核提供的无锁快照读机制(解决读写冲突),乐观锁是应用层的更新写法(解决写写冲突),两者不是一回事。
- 换问法:若题干问「
SELECT ... FOR UPDATE属于哪一类锁」,答案就是「悲观锁中的排他锁(X 锁)」。
【自测】 表 account(id, balance, version) 用乐观锁扣款。两个事务同时读到 version = 5,都要把余额减 100。第二个事务的 UPDATE 会发生什么?
答:影响行数为 0,更新失败。因为
WHERE version = 5已不成立(第一个事务已把version改成 6),应用层需重新读取最新版本后重试。与补-23、主库 M17 连考;互联网面试高频。
【选项误解】
- A 误解来源:两种锁的实现机制完全对调。行锁是悲观锁手段,版本号是乐观锁手段。
- B 正确项:冲突假设 + 加锁时机 + 实现手段三点齐全。
- C 误解来源:绝对化。高冲突下乐观锁重试风暴可能更慢。
- D 误解来源:乐观锁本质是应用层约定,数据库不提供「乐观锁」这种锁类型。
【知识关联】
- 主库关联:M17(无索引 UPDATE 的锁行为——悲观锁底层代价)、M09/M32(隔离级别影响锁行为)、补-18(2PL)、补-23(死锁——悲观锁常见副作用)、补-19(S/X 锁)。
- 国网/408:国网第 13 条并发控制的应用延伸;面试比笔试更常考。
- 面试追问:① 乐观锁失败应用层怎么做?→ 捕获影响行数 0,重新读版本后重试(注意幂等与退避)。②
SELECT ... FOR UPDATE与UPDATE ... WHERE version=?如何选?→ 冲突高用前者,读多写少用后者。③ Redis WATCH 算乐观还是悲观?→ 乐观(CAS 思想)。④ MVCC 是乐观锁吗?→ 不是,MVCC 解决读写冲突的快照读;乐观锁解决写写冲突。
【拓展延伸】
- 变式问法:①「先加锁再操作」→ 悲观;②「更新时校验版本号」→ 乐观;③「
SELECT ... FOR UPDATE属于?」→ 悲观排他锁;④「库存扣减推荐?」→ 悲观锁或UPDATE stock SET n=n-1 WHERE id=? AND n>=1(条件更新,兼具乐观思想)。 - 工程参数/命令:
-- 悲观
BEGIN;
SELECT balance FROM account WHERE id=1 FOR UPDATE;
UPDATE account SET balance=balance-100 WHERE id=1;
COMMIT;
-- 乐观
UPDATE account
SET balance=balance-100, version=version+1
WHERE id=1 AND version=5;
-- 检查 ROW_COUNT(),为 0 则重试
-- 等待与超时
SET SESSION innodb_lock_wait_timeout = 3;- 条件更新精简版(常被面试追问):
UPDATE t SET c=c-1 WHERE id=1 AND c>0;
-- 不显式存 version,也避免了「读-改-写」竞态补-22
InnoDB 的 Buffer Pool 采用改进的 LRU 算法(把 LRU 链分为 young 与 old 两个区域),其主要目的是( )。 A. 提高磁盘 IO 的吞吐能力 B. 让所有数据页都常驻内存 C. 防止全表扫描、预读等「一次性大批量访问」把热点页挤出缓存,避免缓存污染 D. 替代 redo log 承担持久化职责
答案:C
📌 考点定位: MySQL——Buffer Pool 与 LRU 改进,互联网面试高频,主库未覆盖。
【结论】 把 LRU 链切成 young / old 两区,目的是防止全表扫描、预读这类一次性大批量访问把热点页挤出缓存,即避免缓存污染。选 C。
【逐项辨析】
- A 提高磁盘 IO 的吞吐能力:错。Buffer Pool 是内存缓存,它存在的意义是减少磁盘 IO;改进 LRU 解决的是「该淘汰谁」的问题,不是「提高磁盘吞吐」。
- B 让所有数据页都常驻内存:错。内存有限,不可能常驻全部数据页,
innodb_buffer_pool_size只是一个容量上限。 - C 防止全表扫描、预读等「一次性大批量访问」把热点页挤出缓存,避免缓存污染:正确。 这正是改进 LRU 的唯一设计目的。
- D 替代 redo log 承担持久化职责:错。Buffer Pool 是易失的内存结构,持久化由 redo log(WAL)保证。
【知识点】 Buffer Pool 是 InnoDB 在内存中缓存数据页的区域,所有读写都先经过它。若用标准 LRU,一次全表扫描会把大量数据页瞬间灌入链表头部,把链表尾部真正的热点页全部挤出去 —— 这叫缓存污染(buffer pool pollution)。
InnoDB 的改进:把 LRU 链表按比例切成两段。
| 区域 | 位置 | 默认占比 | 存放内容 |
|---|---|---|---|
| young 区(新子链) | 链表头部 | 5/8 | 真正的热点页 |
| old 区(旧子链) | 链表尾部 | 3/8 | 新读入的页、冷数据 |
两个关键机制,缺一不可:
- 新页先插入 old 区头部(不是 young 区头部),只有被再次访问才有资格晋升。
- 晋升有停留时间门槛:该页在 old 区停留超过
innodb_old_blocks_time(默认 1000 毫秒)后再次被访问,才移到 young 区头部;否则(如全表扫描中同一页被快速连续访问)只在 old 区内前移位置,不晋升。
推导链条:全表扫描读入的页全部堆在 old 区 → 扫描结束后很快被淘汰 → young 区里的热点页一根汗毛都没动。所以「分区 + 时间门槛」两者必须同时存在:只分区不设门槛,扫描页会被连续访问而立即晋升;只设门槛不分区,冷页仍与热页混在同一链表里竞争。
补充:young 区头部的页被访问时也不会每次都移到最前(避免热页频繁搬移),需满足条件才移动,进一步降低链表操作开销。
【记忆锚点】 「新页先蹲冷板凳(old 区),熬够 1 秒再上位(young 区)」。
【易混对比】
- Buffer Pool vs redo log:前者是内存缓存(易失,管读性能),后者是磁盘日志(持久,管崩溃恢复),职责完全不同。
- 改进 LRU vs 标准 LRU:标准 LRU 只看「最近是否被访问」,改进 LRU 额外加了「分区 + 时间门槛」两道闸。
innodb_old_blocks_timevsinnodb_buffer_pool_instances:前者控制「冷页熬多久才晋升」,后者把 Buffer Pool 拆成多个实例以减少并发争用。- 换问法:若题干问
innodb_old_blocks_time的作用,答案就是「控制页在 old 区停留多久才有资格晋升 young 区」。
【自测】 把 innodb_old_blocks_time 设为 0,全表扫描造成的缓存污染会变严重还是缓解?
答:变严重。门槛为 0 意味着页进入 old 区后立刻就有资格晋升,扫描期间被访问的页会迅速涌入 young 区,热点页照样被挤出 —— 改进 LRU 的保护效果被削弱。与补-23、主库 M01 连考;互联网面试高频。
【选项误解】
- A 误解来源:把 Buffer Pool 的「存在意义」(减少 IO)安到「改进 LRU 的目的」上。改进解决的是淘汰谁。
- B 误解来源:不现实。内存有限,不可能全量常驻。
- C 正确项:防全表扫描/预读造成缓存污染。
- D 误解来源:Buffer Pool 易失,持久化靠 redo log(WAL),职责完全不同。
【知识关联】
- 主库关联:M01(聚簇/二级索引页是 Buffer Pool 缓存对象)、M02(B+ 树与页)、M13(redo 与脏页刷盘)、M24(深分页优化——扫描压力与缓存)、补-25(ICP 减少无效回表页访问)。
- 国网/408:408 操作系统「页面置换」可对照;国网笔试较少直接考 Buffer Pool,面试极高频。
- 面试追问:① young/old 默认比例?→ 5/8 与 3/8(
innodb_old_blocks_pct,默认 37)。② 晋升门槛参数?→innodb_old_blocks_time默认 1000ms。③ 如何观察命中率?→SHOW ENGINE INNODB STATUS中 Buffer pool hit rate;或information_schema.INNODB_BUFFER_POOL_STATS。
【拓展延伸】
- 变式问法:①「改进 LRU 目的?」→ 防缓存污染;②「
innodb_old_blocks_time作用?」→ old 区页停留多久后再次访问才有资格晋升;③「设为 0 会?」→ 保护失效,扫描页快速涌入 young 区。 - 完整机制推导:
标准 LRU 的问题:
全表扫描瞬间读入大量页 → 全部插到 LRU 头部
→ 业务热点页被挤到尾部淘汰 → 扫描结束后缓存里全是「一次性页」
InnoDB 改进:
LRU = young (5/8) + old (3/8)
新页插入 old 区头部
仅当在 old 区停留 > innodb_old_blocks_time 且再次被访问
→ 才晋升到 young 区头部
效果:
扫描页在 old 区快速过期淘汰
young 区热点页不受冲击- 工程参数/命令:
SHOW VARIABLES LIKE 'innodb_buffer_pool_size'; -- 建议物理内存 50%~70%
SHOW VARIABLES LIKE 'innodb_old_blocks_pct'; -- 默认 37(old 占比)
SHOW VARIABLES LIKE 'innodb_old_blocks_time'; -- 默认 1000
SHOW VARIABLES LIKE 'innodb_buffer_pool_instances'; -- 多实例减锁争用
-- 动态改大小(5.7+/8.0)
SET GLOBAL innodb_buffer_pool_size = 8589934592;补-23
InnoDB 检测到死锁后,默认的处理方式是( )。 A. 主动检测(默认开启 innodb_deadlock_detect),回滚「代价最小」的那个事务,让其余事务继续执行 B. 永久挂起所有相关事务,等待人工干预 C. 直接重启数据库实例以清除死锁 D. 不做任何处理,由应用层超时后自行放弃
答案:A
📌 考点定位: MySQL——死锁检测与处理,互联网面试高频,主库未覆盖。
【结论】 InnoDB 默认开启死锁检测(innodb_deadlock_detect),发现等待成环后回滚「代价最小」的那个事务,其余事务继续执行。选 A。
【逐项辨析】
- A 主动检测(默认开启
innodb_deadlock_detect),回滚「代价最小」的那个事务,让其余事务继续执行:正确。 「主动检测 + 牺牲一个 + 其余放行」正是 InnoDB 的默认行为。 - B 永久挂起所有相关事务,等待人工干预:错。InnoDB 会自动打破死锁,不会永久挂起等人处理。
- C 直接重启数据库实例以清除死锁:错。重启实例是极端运维手段,与死锁的默认处理机制无关。
- D 不做任何处理,由应用层超时后自行放弃:错。
innodb_lock_wait_timeout(默认 50 秒)只是兜底手段,不是默认的第一道处理;InnoDB 默认是主动检测。
【知识点】 死锁:两个或多个事务互相持有对方需要的锁,形成循环等待,谁也无法推进。
InnoDB 的处理流程:
- 构建事务等待图(wait-for graph):顶点是事务,有向边表示「事务 T₁ 正在等待 T₂ 持有的锁」。
- 检测环路:
innodb_deadlock_detect(MySQL 5.7.15 起可显式配置,默认 ON)开启时,每当有事务需要进入锁等待,就检查加入这条等待边后是否成环。 - 选择牺牲者:发现环路后,挑选回滚代价最小的事务(通常按修改行数、undo log 大小衡量)回滚,释放它持有的锁,其余事务得以继续。
- 报错返回:被牺牲的事务收到
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction,应用层必须重试。 - 超时兜底:若关闭死锁检测(
innodb_deadlock_detect = OFF,可减少高并发下的检测开销),则退化为靠innodb_lock_wait_timeout(默认 50 秒)超时释放。
| 机制 | 触发条件 | 后果 | 是否默认 |
|---|---|---|---|
| 死锁检测 | 事务等待图成环 | 回滚牺牲者,其余事务继续 | ✅ 默认开启 |
| 锁等待超时 | 等待超过 innodb_lock_wait_timeout | 当前语句回滚并报错 | 兜底(默认 50 秒) |
预防死锁的四条实践:① 统一加锁顺序(如都按主键升序更新);② 缩短事务(快进快出,减少持锁时间);③ 给 WHERE 条件列建索引(避免行锁退化为锁全表);④ 必要时降低隔离级别(RR → RC 可减少间隙锁)。
【记忆锚点】 「死锁不等待,回滚代价最小的那个,其余照常走」——被牺牲的事务要由应用层重试。
【易混对比】
- 死锁 vs 锁等待:锁等待是「暂时排不上队」(超时才报错),死锁是「互相卡死」(必须回滚一个)。
innodb_deadlock_detectvsinnodb_lock_wait_timeout:前者是主动检测(默认开),后者是超时兜底(默认 50 秒)。关闭检测能省 CPU,代价是把死锁退化成慢超时。- 死锁 vs 脏读 / 幻读:前者是并发调度问题,后者是隔离级别问题。
- 换问法:若题干问「死锁被回滚后应用该怎么办」,答案是「捕获 1213 错误并重试整个事务」。
【自测】 事务 A 先更新 id = 1 再更新 id = 2,事务 B 先更新 id = 2 再更新 id = 1,两者并发执行会怎样?怎么改代码避免?
答:形成死锁,InnoDB 回滚其中一个事务(报 1213),另一个继续执行;避免办法是统一按
id升序加锁(如都先更新id = 1再更新id = 2)。与补-21、主库 M17、M23 连考;互联网面试高频。
【选项误解】
- A 正确项:默认开启检测,回滚代价最小者,其余继续。
- B 误解来源:以为要人工运维介入。InnoDB 自动打破死锁。
- C 误解来源:重启实例是极端手段,不是默认机制。
- D 误解来源:把
innodb_lock_wait_timeout兜底当成默认第一策略。默认是主动检测。
【知识关联】
- 主库关联:M17(无索引更新导致锁升级/范围扩大→死锁温床)、M18(意向锁)、M22/M23(主键设计影响加锁顺序)、补-18(2PL 不防死锁)、补-19(锁相容)、补-21(悲观锁场景)。
- 国网/408:408 有死锁检测(资源分配图/等待图);国网第 13 条并发控制。
- 面试追问:① 错误码?→
ERROR 1213 (40001),应用需重试整个事务。② 高并发下为何有人关闭 deadlock detect?→ 检测本身有 CPU 开销;关闭后靠 50s 超时,延迟变差。③ 如何预防?→ 统一加锁顺序、短事务、WHERE 建索引、必要时降隔离级别。
【拓展延伸】
- 变式问法:①「InnoDB 死锁默认?」→ 检测+回滚代价最小事务;②「等待图有环说明?」→ 死锁;③「应用收到 1213?」→ 捕获并重试;④「关闭检测的代价?」→ 死锁退化为长超时。
- 完整流程推导:
T1: UPDATE ... WHERE id=1; -- 持有 id=1 的 X
T2: UPDATE ... WHERE id=2; -- 持有 id=2 的 X
T1: UPDATE ... WHERE id=2; -- 等 T2
T2: UPDATE ... WHERE id=1; -- 等 T1
等待图:T1→T2→T1 成环
InnoDB:
选择 undo 数量/修改行数更小的事务回滚
返回 1213
另一事务获得锁继续
预防代码:
两事务都先更新 id=1 再更新 id=2(统一升序)- 工程参数/命令:
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SET GLOBAL innodb_deadlock_detect = OFF; -- 谨慎
SHOW ENGINE INNODB STATUS\G -- LATEST DETECTED DEADLOCK
SELECT * FROM performance_schema.data_lock_waits; -- 8.0补-24
在 MySQL 中存储「金额」类数据,最合适的字段类型是( )。 A. FLOAT B. DOUBLE C. VARCHAR D. DECIMAL(定点数)
答案:D
📌 考点定位: MySQL——数据类型与表设计,互联网面试高频,主库未覆盖。
【结论】 金额必须用定点数 DECIMAL,二进制浮点数 FLOAT / DOUBLE 无法精确表示多数十进制小数,会导致对账不平。选 D。
【逐项辨析】
- A
FLOAT:错。4 字节单精度二进制浮点数,约 7 位有效数字,精度不可控,累加误差明显。 - B
DOUBLE:错。8 字节双精度,有效数字约 15~16 位,看似够用,但本质仍是二进制浮点,0.1 + 0.2 ≠ 0.3这类误差照样存在。 - C
VARCHAR:错。类型错配——字符串无法做数值运算与范围排序,还会引入隐式类型转换(比较结果依赖转换规则)。 - D
DECIMAL(定点数):正确。 按「整数位 + 小数位」精确存储,DECIMAL(10,2)表示共 10 位、其中小数 2 位,精确到分。
【知识点】 数值类型分两类:
| 类型 | 存储方式 | 精度 | 适用场景 |
|---|---|---|---|
FLOAT / DOUBLE | 二进制浮点(IEEE 754) | 近似值,无法精确表示多数十进制小数 | 科学计算、传感器数据 |
DECIMAL(M,D) | 定点数,按位精确存储 | 精确值 | 金额、汇率、税额 |
根本原因:十进制小数 0.1 转成二进制是无限循环小数 0.0001100110011…,只能截断存储,于是累加与比较都出现误差。例如 FLOAT 下 0.1 + 0.2 的结果约为 0.30000001192092896,在金额场景下就是「分」级别的对账差错。
DECIMAL(M,D) 的位数规则:M 为总位数(1~65),D 为小数位数(0~30),且必须 D ≤ M,默认 DECIMAL(10,0)。MySQL 5.0.3 起 DECIMAL 改为二进制格式存储(每 9 位十进制数用 4 字节),同一版本也把上限定为 M ≤ 65、D ≤ 30;比早期按字符串存储更省空间。注意版本归属是 5.0.3,不是 5.7。
【记忆锚点】 「金额用 DECIMAL,浮点只算科学」。
【易混对比】
DECIMALvsFLOAT/DOUBLE:前者精确、运算略慢;后者近似、运算快。DECIMALvs 用BIGINT存「分」:两者都能精确,后者更省空间、运算更快,但需在业务层做单位换算 —— 都是工程常见方案。DECIMAL(10,2)的含义:总位数 10、小数位 2,即最大99999999.99;写成DECIMAL(2,10)是非法的(小数位不能大于总位数)。- 换问法:若题干问「为什么不推荐用
DOUBLE存金额」,答案就是「二进制浮点是近似值,累加会产生误差导致对账不平」。
【自测】 把金额字段从 DECIMAL(10,2) 改成 DOUBLE,SELECT SUM(amount) 会出现什么现象?
答:总和出现浮点误差(如
100.00变成99.99999999999999),无法与业务系统对账。与补-25 的表设计考点连考;互联网面试高频。
【选项误解】
- A 误解来源:以为「小数类型」都行。FLOAT 是二进制浮点,约 7 位有效数字,金额会累加误差。
- B 误解来源:以为 DOUBLE 精度够就万事大吉。本质仍是 IEEE754 二进制浮点,
0.1+0.2≠0.3。 - C 误解来源:类型错配。字符串不能直接数值运算/正确数值排序,还可能触发隐式转换。
- D 正确项:DECIMAL 定点数按位精确,适合金额。
【知识关联】
- 主库关联:M26(char/varchar 类型选择——表设计)、M08(索引前缀与类型)、补-12~14(表设计规范化后仍要选对类型)。
- 国网/408:408 组成原理/数据库都涉及浮点表示;国网表设计相关考点。
- 面试追问:① 为何不用 DOUBLE 存钱?→ 二进制无法精确表示多数十进制小数,对账不平。② BIGINT 存「分」可以吗?→ 可以,更省更快,业务层换算。③ DECIMAL(10,2) 什么意思?→ 共 10 位数字、小数 2 位,最大 99999999.99。
【拓展延伸】
- 变式问法:①「金额字段选?」→ DECIMAL 或 BIGINT(分);②「科学计算/传感器?」→ DOUBLE 可以;③「DECIMAL(7,3) 范围?」→ 总 7 位小数 3 位,整数最多 4 位。
- 完整误差推导:
十进制 0.1 = 二进制 0.0001100110011...(循环)
IEEE754 只能有限位截断
故 float/double 的 0.1 实际是近似值
累加 100 次 0.1:精确应为 10.00
浮点结果可能为 9.999999... 或 10.0000001
金额场景差 1 分即对账失败
DECIMAL(M,D):按十进制位精确定点存储(5.0.3 起二进制打包,每 9 位→4 字节),运算精确- 工程参数/命令:
CREATE TABLE order_pay (
id BIGINT PRIMARY KEY,
amount DECIMAL(12,2) NOT NULL DEFAULT 0.00
);
-- 或
amount_cent BIGINT NOT NULL DEFAULT 0; -- 单位:分
SELECT CAST(0.1+0.2 AS DECIMAL(10,2)); -- 0.30 精确补-25
MySQL 5.6 引入的「索引下推」(Index Condition Pushdown, ICP)的作用是( )。 A. 把索引全部加载到内存,以减少磁盘 IO B. 在存储引擎层就利用索引中的列对不满足条件的记录提前过滤,从而减少回表次数 C. 自动为所有 WHERE 条件列创建索引 D. 把查询结果缓存到 Buffer Pool 中
答案:B
📌 考点定位: MySQL——索引下推,互联网面试高频,主库未覆盖;与主库 M04(覆盖索引)、M05(最左前缀)共同构成索引体系。
【结论】 索引下推(ICP)把「索引里已有的列能判断的条件」下推到存储引擎层提前过滤,只让真正满足条件的记录回表,从而减少回表次数。选 B。
【逐项辨析】
- A 把索引全部加载到内存,以减少磁盘 IO:错。这是 Buffer Pool 的职责,与 ICP 无关。
- B 在存储引擎层就利用索引中的列对不满足条件的记录提前过滤,从而减少回表次数:正确。 「存储引擎层过滤」与「减少回表」是 ICP 的两个关键词。
- C 自动为所有
WHERE条件列创建索引:错。MySQL 不会自动建索引,索引必须显式创建。 - D 把查询结果缓存到 Buffer Pool 中:错。Buffer Pool 缓存的是数据页(不是查询结果),且 MySQL 8.0 已移除查询缓存(query cache)。
【知识点】 MySQL 架构分两层:Server 层(解析、优化、执行)与存储引擎层(InnoDB,负责读写数据页)。ICP 的本质就是把过滤动作从 Server 层下推到引擎层。
以 SELECT * FROM t WHERE name LIKE 'a%' AND age = 20、索引 idx(name, age) 为例:
无 ICP:
- 存储引擎用索引定位到所有
name LIKE 'a%'的索引记录; - 对每一条记录按主键回表,取出完整行;
- 把整行交给 Server 层,Server 层再判断
age = 20; - 不满足的记录被丢弃 —— 回表白做了。
有 ICP(MySQL 5.6 引入,默认开启):
- 存储引擎定位到索引记录后,直接在索引上取出
age列做判断; - 只有同时满足
name LIKE 'a%'与age = 20的记录才回表; - 回表次数显著下降。
| 维度 | 无 ICP | 有 ICP |
|---|---|---|
过滤 age = 20 的位置 | Server 层 | 存储引擎层 |
| 回表次数 | 所有 name 命中的记录都回表 | 仅两个条件都命中的记录回表 |
| 收益 | — | 减少回表、减少两层之间的交互 |
生效条件(三条同时满足):
- 访问类型为
range、ref、eq_ref、ref_or_null(即EXPLAIN的type列); - 条件涉及联合索引中的后续列(如
(name, age)里的age),且该条件无法用于缩小索引扫描范围; - 存储引擎支持 ICP(InnoDB、MyISAM 均支持)。
如何判断生效:EXPLAIN 的 Extra 列出现 Using index condition。 必须区分:Using index 表示覆盖索引(不需要回表);Using index condition 表示ICP(用索引下推减少了回表,但仍可能回表)。两者最常被混淆。
【知识点扩写·ICP 生效与失效】
| 条件 | 生效? | 说明 |
|---|---|---|
| type=range/ref/eq_ref | ✓ | 前提 |
| type=ALL 全表扫描 | ✗ | 无索引可用 |
| 条件列不在索引中 | ✗ | 引擎层读不到该列 |
| 覆盖索引已免回表 | 无需 ICP | 更优路径 |
| 条件已用于缩小扫描范围 | 可能不体现 | 优化器已在索引层解决 |
注意:ICP 是 MySQL 5.6+ 的优化器/执行器协作特性,默认开启;分库分表中间件不一定下推。
【记忆锚点】 「ICP 就是把筛选提前到引擎里做,能在索引上拦下的绝不回表」——Extra 里出现 Using index condition 就是它。
【易混对比】
- ICP vs 覆盖索引:覆盖索引是「不用回表」(索引里已含所需全部列);ICP 是「少回表」(在索引里先过滤掉一部分)。
Using index↔ 覆盖索引,Using index condition↔ ICP。 - ICP vs 最左前缀:最左前缀决定「能不能用上索引」,ICP 决定「用上之后能不能少回表」。
- 下推 vs 上移:ICP 是把 Server 层的判断下推给引擎层,方向不要记反。
- 换问法:若题干问「
Extra显示Using index condition说明什么」,答案就是「启用了索引下推」。
【自测】 表 t(id 主键, name, age, addr),索引 idx(name, age)。把查询改成 SELECT name, age FROM t WHERE name LIKE 'a%' AND age = 20,Extra 会显示什么?还需要回表吗?
答:显示
Using index(覆盖索引),不需要回表 —— 因为name、age两列都在索引里。与主库 M04、M05 连考;互联网面试高频。
【选项误解】
- A 误解来源:把 Buffer Pool 的职责安到 ICP 上。
- B 正确项:引擎层用索引列提前过滤,减少回表。
- C 误解来源:MySQL 从不自动建索引。
- D 误解来源:Buffer Pool 缓存的是页不是结果集;且 8.0 已移除 query cache。
【知识关联】
- 主库关联:M04(覆盖索引 Using index)、M05(最左前缀)、M06(索引失效写法)、M19(EXPLAIN type)、M20(Extra 字段)、补-22(Buffer Pool)。ICP 是索引体系中「少回表」的关键一环。
- 国网/408:国网笔试较少;互联网面试高频。
- 面试追问:①
Using indexvsUsing index condition?→ 前者覆盖索引不回表;后者 ICP,仍可能回表但次数更少。② ICP 生效条件?→ range/ref/eq_ref 等 + 条件涉及联合索引后续列 + 引擎支持。③ 与 MRR 关系?→ Multi-Range Read 把随机回表整理成顺序,可与 ICP 叠加收益。
【拓展延伸】
- 变式问法:①「ICP 的作用?」→ 引擎层提前过滤,减少回表;②「Extra: Using index condition 说明?」→ 启用了索引下推;③「如何减少回表?」→ 覆盖索引、ICP、延迟关联。
- 完整执行对比推导:
表 t(id PK, name, age, addr),索引 idx(name, age)
SQL: SELECT * FROM t WHERE name LIKE 'a%' AND age=20
无 ICP:
引擎按 name LIKE 'a%' 扫 idx → 每条回表 → Server 再判 age
回表次数 ≈ name 前缀命中行数
有 ICP:
引擎在 idx 上直接判 age(age 已在索引里)
仅 age=20 的记录回表
回表次数 ≈ 同时满足两条件的行数
改写成 SELECT name, age:
Extra=Using index(覆盖索引,0 回表)- 工程参数/命令:
EXPLAIN SELECT * FROM t WHERE name LIKE 'a%' AND age=20;
-- Extra: Using index condition → ICP
-- Extra: Using index → 覆盖索引
SET optimizer_switch='index_condition_pushdown=on'; -- 默认 on
-- 延迟关联深翻页优化见主库 M24补-26
用 Redis 实现「令牌桶」限流,其核心思想是( )。 A. 每个请求都直接拒绝,不放入任何队列 B. 在固定时间窗口内计数,超过阈值即拒绝,但窗口边界处可能出现两倍流量 C. 以恒定速率向桶中放入令牌,请求取到令牌才被放行;桶的容量决定了允许的突发流量大小 D. 用 KEYS * 统计当前请求数后再做判断
答案:C
📌 考点定位: Redis——限流算法,互联网面试高频,主库未覆盖。
【结论】 令牌桶以恒定速率往桶里放令牌、桶有容量上限、请求取到令牌才放行;桶内可以积攒令牌,因此允许一定程度的突发流量。选 C。
【逐项辨析】
- A 每个请求都直接拒绝,不放入任何队列:错。这不是限流算法,而是「全拒绝」,等同于服务不可用。
- B 在固定时间窗口内计数,超过阈值即拒绝,但窗口边界处可能出现两倍流量:错。这句描述的本身就是固定窗口计数算法(而且把它的缺点当成了定义),不是令牌桶。
- C 以恒定速率向桶中放入令牌,请求取到令牌才被放行;桶的容量决定了允许的突发流量大小:正确。 完整点出了「恒速放令牌 + 桶容量上限 + 消耗令牌放行」三个要素。
- D 用
KEYS *统计当前请求数后再做判断:错。KEYS *是阻塞式危险命令(遍历全部 key、阻塞 Redis 主线程),线上禁用;它也根本不构成限流算法。
【知识点】 四大限流算法对比:
| 算法 | 核心机制 | 是否允许突发 | 主要问题 |
|---|---|---|---|
| 固定窗口计数 | 每个固定时间段内计数,超阈值拒绝 | ❌ | 窗口边界可能出现 2 倍流量 |
| 滑动窗口 | 窗口切细并滚动,或用 ZSet 记录请求时间戳 | 有限 | 边界平滑,但存储与计算开销大 |
| 漏桶 | 请求先入桶,再以恒定速率流出 | ❌ 严格不允许 | 流量绝对平滑,突发请求被排队或丢弃 |
| 令牌桶 | 以恒定速率生成令牌入桶,桶有容量上限,请求消耗令牌 | ✅ 允许 | 突发量受桶容量约束 |
令牌桶的两个参数:生成速率 r(每秒放多少令牌,等于长期平均 QPS 上限)与桶容量 C(最多能攒多少令牌,等于允许的瞬时突发量)。长期平均流量被 r 限制,短时间内则可以有 C 个请求一起通过。
与漏桶的本质区别:漏桶的流出速率恒定(关注「平滑」),令牌桶的生成速率恒定(关注「限速 + 容忍突发」)。工程实现中最常用的就是令牌桶,Guava 的 RateLimiter 即其实现。
Redis 实现要点:用 INCR + EXPIRE 做固定窗口最省事;令牌桶与滑动窗口需用 Lua 脚本把「取令牌 / 更新计数」的读改写打包成原子操作,否则并发下会超发。
【记忆锚点】 「令牌桶允许突发,漏桶平滑恒定」——桶里攒的是「额度」,攒得多就能一次花掉。
【易混对比】
- 令牌桶 vs 漏桶:前者允许突发(桶内可积攒令牌),后者不允许突发(出水速率恒定)。
- 令牌桶 vs 固定窗口:固定窗口有边界双倍流量问题;令牌桶靠连续补充令牌,天然没有边界问题。
- 限流 vs 熔断 vs 降级:限流是「控制进入的流量」,熔断是「下游故障时快速失败」,降级是「牺牲非核心功能」,三者常配合使用。
- 换问法:若题干问「哪种算法流量最平滑」,答案是漏桶;问「哪种允许突发」,答案是令牌桶。
【自测】 接口限流配置为「令牌桶容量 100、生成速率 10 个/秒」。某一秒内突然涌入 100 个请求,会被拒绝吗?长期能扛住的平均 QPS 是多少?
答:前 100 个请求都能通过(桶里已攒满令牌,瞬间取完);但长期平均 QPS 上限是 10,桶耗尽后超出的请求被拒绝。与主库 R01 的 Redis 应用场景连考;互联网面试高频。
【选项误解】
- A 误解来源:把「限流」理解成「全拒绝」,这不是算法。
- B 误解来源:B 项描述的是固定窗口计数算法本身(还顺带写了它的边界双倍缺点),张冠李戴。
- C 正确项:恒速放令牌 + 桶容量上限 + 取到才放行;容量决定突发上限。
- D 误解来源:
KEYS *是危险阻塞命令,更不是限流算法。
【知识关联】
- 主库关联:R16(不合理做法/阻塞)、R26(单线程下易阻塞操作)、R25(Pipeline 减少 RTT)、R21(Lua 保证原子)、R28(热 key)。
- 国网/408:408 不直接考 Redis 限流;面试/网关场景高频。
- 面试追问:① 固定窗口边界问题?→ 窗口切换瞬间可能通过 2×阈值流量。② 令牌桶与漏桶区别?→ 前者允许突发(桶内可积攒),后者出流恒定。③ 如何保证限流原子性?→ Lua 脚本打包读改写;或 Redisson RateLimiter。
【拓展延伸】
- 变式问法:①「允许突发的算法?」→ 令牌桶;②「流量最平滑?」→ 漏桶;③「窗口边界双倍流量?」→ 固定窗口;④「ZSet 记时间戳实现?」→ 滑动窗口。
- 工程参数/命令(令牌桶 Lua 示意):
-- KEYS[1]=bucket, ARGV: capacity, rate, now, need
local tokens = tonumber(redis.call('HGET', KEYS[1], 't') or ARGV[1])
local last = tonumber(redis.call('HGET', KEYS[1], 'ts') or ARGV[3])
local delta = math.max(0, ARGV[3]-last) * tonumber(ARGV[2])
tokens = math.min(tonumber(ARGV[1]), tokens+delta)
if tokens >= tonumber(ARGV[4]) then
tokens = tokens - tonumber(ARGV[4])
redis.call('HSET', KEYS[1], 't', tokens, 'ts', ARGV[3])
return 1
end
redis.call('HSET', KEYS[1], 't', tokens, 'ts', ARGV[3])
return 0- 网关对照:Nginx
limit_req(漏桶/令牌桶思想)、Sentinel/Resilience4j RateLimiter、Guava RateLimiter(单机令牌桶)。 - 固定窗口快速实现:
INCR rate:{uid}:{window}
若返回 1 则 EXPIRE 秒级 TTL
若返回 > threshold 则拒绝
简单但边界不平滑补-27
下列业务场景与 Redis 数据结构的匹配,不正确的一项是( )。 A. 排行榜(按分数排序、取 Top N)——ZSet B. 购物车(存储商品的多个字段)——Hash C. 消息队列 / 最新 N 条时间线——List D. 统计独立访客(UV)并支持精确去重计数——ZSet
答案:D
📌 考点定位: Redis——数据结构与应用场景,互联网面试高频(对应主库 R01 的应用场景补充)。
【结论】 统计独立访客(UV)并精确去重计数应当用 Set(或 HyperLogLog 做近似去重),用 ZSet 属于结构错配 —— 题干问的是「不正确」的一项,故选 D。
【逐项辨析】
- A 排行榜(按分数排序、取 Top N)——ZSet:匹配正确。ZSet 的
score天然承载排序权重,ZREVRANGE key 0 N-1取 Top N 为O(log N + N)(N 为返回条数,别只记O(log N))。 - B 购物车(存储商品的多个字段)——Hash:匹配正确。Hash 的「field → value」正好对应「商品 id → 商品信息(数量、规格等)」。
- C 消息队列 / 最新 N 条时间线——List:匹配正确。List 双端插入删除均为 O(1),
LPUSH+LRANGE/LTRIM天然适配队列与时间线。 - D 统计独立访客(UV)并支持精确去重计数——ZSet:匹配错误,即本题所选的「不正确」项。 精确去重应使用 Set(
SADD天然去重、SCARD计数);ZSet 每个成员还要额外维护一个score,内存开销更大,而score对 UV 统计毫无用处。
【知识点】 Redis 五大数据类型的能力边界:
| 类型 | 底层结构(小规模 → 大规模) | 核心能力 | 典型场景 |
|---|---|---|---|
| String | int / embstr → raw | 单值、计数(INCR) | 缓存对象、计数器、分布式锁 |
| Hash | listpack → hashtable | 字段级读写(HSET / HGET) | 购物车、用户信息、对象缓存 |
| List | listpack → quicklist | 双端 O(1) 插入删除、有序 | 消息队列、最新 N 条时间线 |
| Set | intset / listpack → hashtable | 去重、交并差(SINTER / SUNION) | UV 去重、标签、共同好友 |
| ZSet | listpack → skiplist + dict | 按 score 排序、范围查询、排名 | 排行榜、延迟队列、带权重的 Top N |
「精确去重计数」的三种解法与取舍:
| 方案 | 关键命令 | 精确性 | 内存开销 |
|---|---|---|---|
| Set | SADD / SCARD | ✅ 精确 | 与元素数量成正比,大 UV 场景很吃内存 |
| HyperLogLog | PFADD / PFCOUNT | ❌ 近似(标准误差约 0.81%) | 固定约 12 KB,极省 |
| Bitmap | SETBIT / BITCOUNT | ✅ 精确(要求 id 为连续整数) | 与最大 id 成正比 |
选型判据:要精确且元素量可控 → Set;只要量级、要省内存 → HyperLogLog;用户 id 是连续整数 → Bitmap。
【记忆锚点】 「排序找 ZSet,去重找 Set,对象找 Hash,队列找 List,计数找 String」——先问「我要什么能力」,再选结构。
【易混对比】
- Set vs ZSet:Set 只有成员(去重 + 集合运算),ZSet 是「成员 + 分数」(去重 + 排序);ZSet 多出来的
score就是额外内存开销。 - Set vs HyperLogLog:前者精确但内存随元素增长,后者近似但固定约 12 KB。UV 上亿时基本只能选 HyperLogLog。
- List vs ZSet 做延迟队列:List 只能顺序消费;ZSet 用时间戳作
score,可按「到期时间」范围拉取,更适合延迟任务。 - 注意反向提问:题干问「不正确的一项」,四个选项里三个都是对的 —— 答题前先圈出问法,这是笔试高频陷阱。
【自测】 要统计「某商品页一天的独立访客数」,UV 量级在千万以上,且允许 1% 以内的误差。应选哪种 Redis 结构?
答:HyperLogLog(
PFADD/PFCOUNT)。固定约 12 KB 内存、标准误差约 0.81%,满足「千万量级 + 1% 误差」的要求;若必须精确则只能退化为 Set,内存代价极高。与主库 R01、R02 连考;互联网面试高频。
【选项误解】
- A 匹配正确:ZSet score 排序 + ZREVRANGE TopN。
- B 匹配正确:Hash field→value 对应商品 id→属性。
- C 匹配正确:List 双端操作 O(1),LPUSH+LTRIM 做最新 N 条。
- D 匹配错误(本题要选的「不正确」):精确去重计数应使用 Set(SADD/SCARD)或 Bitmap;ZSet 多余 score 纯开销。题干要求「不正确」,故选 D。
【知识关联】
- 主库关联:R01(排行榜该用什么结构)、R02(ZSet 底层 skiplist+dict)、R03(SDS)、补-28(底层编码)。
- 国网/408:国网笔试不考 Redis 结构细节;互联网笔试/面试高频。
- 面试追问:① UV 近千万且允许误差?→ HyperLogLog(PFADD/PFCOUNT,约 12KB,误差 ~0.81%)。② ZSet 能否去重?→ 能,member 唯一,但 UV 不需要 score。③ 共同好友用什么?→ Set 的 SINTER。
【拓展延伸】
- 变式问法:①「排行榜 TopN」→ ZSet;②「精确去重」→ Set;③「近似去重省内存」→ HyperLogLog;④「连续整数 id 的 UV」→ Bitmap;⑤「延迟队列」→ ZSet(score=到期时间戳)。
- 工程参数/命令:
# 排行榜
ZADD rank 100 user1
ZREVRANGE rank 0 9 WITHSCORES
ZREVRANK rank user1
# UV 精确
SADD uv:20260115 uid1
SCARD uv:20260115
# UV 近似
PFADD uv:hll uid1
PFCOUNT uv:hll
# Bitmap
SETBIT uv:bm uid 1
BITCOUNT uv:bm- 内存粗估:Set 存 1000 万整数成员可能数百 MB 级;HyperLogLog 固定 12KB。选型先问「精确性 vs 内存」。
补-28
关于 Redis 的底层编码(encoding),下列说法正确的是( )。 A. 小规模数据会使用紧凑编码(如 listpack / ziplist / intset / embstr)以节省内存,达到阈值后转为 skiplist / hashtable / raw 等结构 B. Redis 各数据类型在任何规模下都固定使用同一种底层编码 C. 紧凑编码(ziplist)适合存储超大规模集合,性能优于 hashtable D. 底层编码一旦确定便永远不会转换
答案:A
📌 考点定位: Redis——底层编码机制,互联网面试高频(对应主库 R02/R03 的底层补充)。
【结论】 Redis 对小规模数据采用紧凑编码(listpack / ziplist / intset / embstr)以节省内存,规模超过阈值后自动转换为大结构(hashtable / skiplist / raw)。选 A。
【逐项辨析】
- A 小规模数据会使用紧凑编码(如 listpack / ziplist / intset / embstr)以节省内存,达到阈值后转为 skiplist / hashtable / raw 等结构:正确。 完整点出了「紧凑编码 → 超阈值 → 大结构」这条主线。
- B Redis 各数据类型在任何规模下都固定使用同一种底层编码:错。错在「任何规模」——编码是随规模动态变化的。
- C 紧凑编码(ziplist)适合存储超大规模集合,性能优于 hashtable:错。说反了 —— 紧凑编码要求内存连续,插入删除要搬移数据,规模一大就退化为 O(N),超大集合应当用 hashtable / skiplist。
- D 底层编码一旦确定便永远不会转换:错。编码会自动转换(且转换是单向的)。
【知识点】 Redis 的「数据类型」是对外接口,「底层编码」是内部实现 —— 同一类型在不同规模下用不同编码,以在「内存」与「性能」之间取舍。
| 数据类型 | 小规模编码 | 大规模编码 | 相关配置项(默认值) |
|---|---|---|---|
| String | int(整数值)/ embstr(7.x ≤44 字节) | raw | 分界点:7.x=44 字节;Redis 8 起改为 key+值 ≤ 41 字节(实测判据见下表「版本实测」) |
| Hash | listpack(旧版 ziplist) | hashtable | hash-max-listpack-entries(512)、hash-max-listpack-value(64) |
| List | listpack(单个 quicklist 节点) | quicklist(多节点) | list-max-listpack-size(默认 -2=按字节算,每节点 ≤8 KB,不是 128 个元素) |
| Set | intset(全整数)/ listpack | hashtable | set-max-intset-entries(512)、set-max-listpack-entries(128) |
| ZSet | listpack(旧版 ziplist) | skiplist + dict | zset-max-listpack-entries(128)、zset-max-listpack-value(64) |
(Redis 7.0 起,Hash / ZSet / List 中的 ziplist 已全面被 listpack 取代;listpack 最早在 Redis 5.0 引入。)
版本实测(本机一次性容器,只读命令):
- String 的 embstr 判据:Redis 7.x 看值长度 ≤ 44 字节;redis:8 容器(8.10.2)改成 key 长度+值长度 ≤ 41 字节——key 长 1 时值 ≤40 仍是
embstr、41 起raw;key 长 10 时值 ≤31。所以「44 字节」要带版本前提。 - Redis 7 容器
CONFIG GET实测:hash-max-listpack-entries=512、list-max-listpack-size=-2(不是 128);灌满 200 与 500 个字段后OBJECT ENCODING都仍是listpack。 BF.*/CF.*概率型结构在 redis:8 容器里开箱可用(MODULE LIST无 bloom 模块),CF.DEL可删单元素;8.6 起才有HOTKEYS系列命令(COMMAND DOCS HOTKEYS回 since 8.6.0)。
两种编码的取舍:
- 紧凑编码(listpack / ziplist / intset):内存连续、无指针开销,省内存;但插入删除需搬移数据、查找 O(N),不适合大规模数据。
- 大结构(hashtable / skiplist):查找 O(1) / O(log N)、插入删除友好;但每个元素带指针与哈希表开销,内存占用高。
查看编码的命令:OBJECT ENCODING key(可能返回 listpack、hashtable、skiplist、intset、embstr、raw 等)。
关键提醒:转换是单向的 —— 元素增长会转成大结构,但元素删除后不会自动转回紧凑编码(Redis 不做反向转换,以避免频繁抖动)。
【知识点扩写·为何转换是单向的】
扩容方向:listpack/intset → hashtable/skiplist
触发:元素个数或元素大小超过阈值
原因:紧凑结构插入/查找退化为 O(N),内存连续搬移代价大
缩容方向:通常不自动逆转
原因:避免「反复跨越阈值」导致频繁 realloc 与 rehash 抖动
工程手段:导出再导入(DUMP/RESTORE),或删除 key 后重写
面试加分点:能指出「单向转换」以及 listpack 解决 ziplist 级联更新【记忆锚点】 「小了挤一挤(紧凑编码省内存),大了就摊开(hashtable / skiplist 保性能);转换只往前、不回头」。
【易混对比】
ziplistvslistpack:ziplist每个节点存「前一节点长度」,会导致级联更新(cascade update);listpack只存自身长度,消除了级联更新,是ziplist的替代者。embstrvsraw:都存字符串。embstr把 RedisObject 与 SDS 放在同一块内存(一次分配、只读,适合短字符串),raw分两次分配(适合长字符串或需修改的场景);分界点为 44 字节。intsetvshashtable:Set 元素全为整数且数量少时用intset(有序数组 + 二分查找);一旦加入非整数元素或超过阈值,转为hashtable。- 编码转换 vs 槽迁移:编码转换发生在单机内存内部(本题),与 Cluster 的槽迁移(补-29)完全是两回事。
【自测】 一个 Hash 有 200 个字段,每个字段值都很短。它的编码是什么?若删除到只剩 50 个字段,编码会变回 listpack 吗?
答:编码是
listpack(200 未超过hash-max-listpack-entries默认的 512——本机 Redis 7 容器实测灌 200 与 500 个字段后OBJECT ENCODING都仍是listpack,要灌到 513 个字段才翻成hashtable);删除到 50 个字段后不会自动变回listpack之外的紧凑编码,也不会从hashtable退回listpack—— 编码转换是单向的,只会"紧凑→大结构",不反向。与主库 R02、R03、补-27 连考;互联网面试高频。
【选项误解】
- A 正确项:小规模紧凑编码,超阈值转大结构。
- B 误解来源:以为类型与编码一一固定。实际随规模动态变化。
- C 误解来源:说反了。紧凑编码适合小规模,大集合应 hashtable/skiplist。
- D 误解来源:编码会自动转换,且转换单向(删元素不回转)。
【知识关联】
- 主库关联:R02(ZSet 底层 skiplist+dict)、R03(SDS 优势)、R04(Redis 为何快——编码紧凑减少内存与 CPU)、补-27(结构选型)。
- 国网/408:408 数据结构(跳表/哈希)可对照;Redis 本身属面试向。
- 面试追问:①
OBJECT ENCODING key看什么?→ 当前底层编码。② listpack 与 ziplist 区别?→ listpack 存自身长度,消除级联更新。③ embstr/raw 分界?→ 7.x 为 44 字节;Redis 8 起改为 key+值 ≤ 41 字节(本机 8.10.2 实测),答 44 要带版本前提(RedisObject+SDS 连续分配的权衡)。
【拓展延伸】
- 变式问法:①「Hash 小规模编码?」→ listpack;②「全整数小 Set?」→ intset;③「超过阈值后?」→ hashtable/skiplist/raw;④「删除后会自动变回紧凑吗?」→ 不会。
- 工程参数/命令:
OBJECT ENCODING mykey
CONFIG GET hash-max-listpack-entries
CONFIG GET zset-max-listpack-value
CONFIG GET set-max-intset-entries
# Redis 7 多数 ziplist 已被 listpack 替代
# 自检脚本:扫描大 key 与编码
redis-cli --bigkeys
redis-cli --memkeys- 阈值默认(常见):
| 类型 | 配置 | 默认 |
|---|---|---|
| Hash | hash-max-listpack-entries | 512 |
| Hash value | hash-max-listpack-value | 64 |
| ZSet | zset-max-listpack-entries | 128 |
| Set intset | set-max-intset-entries | 512 |
| String embstr | (硬编码) | 44 字节 |
补-29
在 Redis Cluster 中,节点返回的 MOVED 与 ASK 两种重定向,其区别是( )。 A. 两者完全等价,可以互换使用 B. MOVED 表示槽已永久迁移到目标节点(客户端应更新本地槽映射);ASK 表示槽正在迁移中,仅本次请求需临时转发,客户端不应更新槽映射 C. MOVED 用于读请求,ASK 用于写请求 D. ASK 表示该槽不存在,需要新建槽
答案:B
📌 考点定位: Redis——Cluster 槽迁移与重定向,互联网面试高频(对应主库 R24 的补充)。
【结论】 MOVED 是永久重定向(槽已迁走,客户端应更新本地槽映射);ASK 是一次性临时重定向(槽正在迁移中,仅本次请求转发,客户端不应更新槽映射)。选 B。
【逐项辨析】
- A 两者完全等价,可以互换使用:错。两者语义完全不同(永久 vs 临时),客户端的处理动作也完全不同。
- B
MOVED表示槽已永久迁移到目标节点(客户端应更新本地槽映射);ASK表示槽正在迁移中,仅本次请求需临时转发,客户端不应更新槽映射:正确。 完整覆盖了「触发时机 + 是否更新映射」两个判据。 - C
MOVED用于读请求,ASK用于写请求:错。两者的区别与读写无关,只与「槽的迁移状态」有关。 - D
ASK表示该槽不存在,需要新建槽:错。ASK只表示「这个槽正在迁移」,槽本身是存在且有归属的。
【知识点】 Redis Cluster 把整个键空间划分为 16384 个槽(slot),槽按 CRC16(key) mod 16384 计算后分配给各节点。客户端访问某个 key 时,若该槽不归当前节点管,节点会返回重定向信息;该槽仍归本节点但正在迁移时也可能被重定向,具体分三种情形(见下表)。
| 情形 | 源节点状态 | 返回 | 客户端动作 |
|---|---|---|---|
| key 的槽不在本节点,且已稳定归属别处 | 该槽已完全迁出 | MOVED <slot> <ip>:<port> | 更新本地槽映射,此后该槽的请求直接发往新节点 |
| key 的槽在本节点、槽正在迁移,但该 key 已迁走 | 槽处于 MIGRATING 状态(槽归属尚未变更) | ASK <slot> <ip>:<port> | 先向目标节点发送 ASKING,再转发本次请求;不更新槽映射 |
| key 的槽在本节点、槽正在迁移,但该 key 尚未迁走 | 槽处于 MIGRATING 状态 | 不重定向(本节点直接执行并返回结果) | 无需重定向处理,正常拿到回复 |
为什么 ASK 必须先发 ASKING?因为目标节点此时只是临时接管这个 key,它自己也认为该槽仍属于源节点。ASKING 是一次性的「放行令牌」,只对紧接着的下一条命令生效。
槽迁移的完整流程(以 redis-cli --cluster reshard 为例):
- 目标节点执行
CLUSTER SETSLOT <slot> IMPORTING <源节点 id>; - 源节点执行
CLUSTER SETSLOT <slot> MIGRATING <目标节点 id>; - 源节点对槽内每个 key 执行
MIGRATE,逐个搬走; - 迁移完成后,新的槽归属在各节点间广播更新。
迁移期间源节点收到请求时:key 还在本节点 → 直接处理,不重定向;key 已迁到目标节点 → 返回 ASK(此时槽的归属还没变,客户端不能改映射);只有该槽已不再归属本节点(迁移完成、新归属已广播)才返回 MOVED。
Cluster 的扩容缩容正是通过在节点间迁移槽实现的,而不需要重算全部 key 的归属 —— 这是「槽」相对普通一致性哈希的改进点。
【记忆锚点】 「MOVED 是搬家搬完了(改地址本);ASK 是搬家搬到一半(这次先帮你转过去,地址本别动)」。
【易混对比】
MOVEDvsASK:判据是「槽的迁移是否完成」。槽归属已变更(迁移完成)→MOVED+ 更新映射;槽仍归本节点、只是这个 key 已迁到目标节点 →ASK+ 不更新映射(key 还在源节点时由源节点直接处理,不重定向)。ASKvsASKING:ASK是服务端返回的重定向响应;ASKING是客户端发送的命令(一次性放行令牌)。- 槽 vs 一致性哈希环:槽是「固定 16384 个格子」,迁移槽即可扩容,影响面可控;一致性哈希环靠虚拟节点分摊,key 归属由哈希环决定。
- 客户端直连 vs 代理模式:Jedis Cluster 等直连客户端需自己维护槽映射并处理重定向;Codis / Twemproxy 等代理模式对客户端透明。
- 换问法:若题干问「客户端收到
ASK后要不要更新槽缓存」,答案是「不要」。
【自测】 Cluster 正在把槽 3000 从节点 A 迁往节点 B(槽仍归 A 所有)。客户端向 A 请求槽 3000 中一个已迁到 B 的 key,A 会返回什么?客户端下一步怎么做?
答:A 返回
ASK 3000 <B 的地址>;客户端先向 B 发送ASKING,再把本次请求转发给 B,且不更新本地槽映射。与主库 R24 连考;互联网面试高频。
【选项误解】
- A 误解来源:以为都是重定向可互换。语义与客户端动作完全不同。
- B 正确项:MOVED 永久+更新映射;ASK 临时+不更新映射。
- C 误解来源:与读写无关,只与槽迁移状态有关。
- D 误解来源:ASK 表示槽正在迁移,不是「槽不存在」。
【知识关联】
- 主库关联:R24(Cluster 数据分片机制——16384 槽 CRC16)、R22(主从复制)、R23(Sentinel 与 Cluster 的定位差异)、补-28(节点内部编码,与槽迁移无关)。
- 国网/408:408 分布式/一致性哈希可对照;Redis Cluster 属面试高频。
- 面试追问:① 为什么是 16384 槽?→ 作者论证:心跳包大小与迁移粒度的折中(槽太多元数据大,太少迁移不均)。② 收到 ASK 下一步?→ 先发 ASKING,再发原命令,不更新槽缓存。③ 客户端模式?→ 直连(Jedis/Lettuce Cluster)自己维护槽表;代理(Codis/Twemproxy)对客户端透明。
【拓展延伸】
- 变式问法:①「槽已迁完,节点返回?」→ MOVED,客户端更新映射;②「迁移中且 key 已迁走?」→ ASK(客户端先发 ASKING、不更新映射);③「迁移中但 key 还在源节点?」→ 源节点直接处理,不重定向;④「ASKING 作用?」→ 一次性放行令牌;⑤「槽归属计算?」→
CRC16(key) mod 16384。 - 工程参数/命令:
redis-cli -c -h host -p 7000 # -c 自动处理重定向
CLUSTER INFO
CLUSTER NODES
CLUSTER KEYSLOT mykey
CLUSTER COUNTKEYSINSLOT 3000
CLUSTER GETKEYSINSLOT 3000 10
# 迁移
redis-cli --cluster reshard host:7000
# 源/目标侧
CLUSTER SETSLOT 3000 IMPORTING <src-id>
CLUSTER SETSLOT 3000 MIGRATING <dst-id>
MIGRATE dst 3000 key 0 5000- 完整迁移时序:
1) 目标:SETSLOT IMPORTING
2) 源:SETSLOT MIGRATING
3) 源逐 key MIGRATE
4) 广播最终槽归属 → 之后稳定返回 MOVED
期间:key 仍在源→源直接处理;key 已迁到目标→ASK(先发 ASKING,不改槽映射);槽归属变更后→MOVED补-30
采用「先更新数据库、再删除缓存」(Cache Aside)策略时,若删除缓存失败,最可靠的补偿手段是( )。 A. 直接忽略失败,等待缓存自然过期 B. 立即回滚数据库事务 C. 通过重试机制 + 消息队列,或订阅数据库 binlog(如 Canal)异步删除缓存,保证最终一致 D. 改为「先删除缓存、再更新数据库」并去掉补偿逻辑
答案:C
📌 考点定位: Redis——缓存与数据库一致性,互联网面试高频(对应主库 R27 的补偿手段补充)。
【结论】 删除缓存失败时,最可靠的补偿是重试机制 + 消息队列或订阅 binlog(如 Canal)异步删除缓存,目标是保证最终一致。选 C。
【逐项辨析】
- A 直接忽略失败,等待缓存自然过期:错。会留下长时间的不一致窗口(长短取决于 TTL),期间读到的一直是脏数据。
- B 立即回滚数据库事务:错。此时数据库更新已经提交,回滚代价极大且可能不可行(其他事务可能已基于新值操作)。
- C 通过重试机制 + 消息队列,或订阅数据库 binlog(如 Canal)异步删除缓存,保证最终一致:正确。 两条补偿路径都指向「保证最终一致」这一目标。
- D 改为「先删除缓存、再更新数据库」并去掉补偿逻辑:错。方向反了,反而引入新的并发脏数据问题 —— 删除缓存后、更新数据库前的读请求会把旧值回填进缓存。
【知识点】 Cache Aside(旁路缓存)是工程上最常用的缓存模式,标准流程:
- 读:先查缓存 → 命中则返回;未命中则查库 → 把结果回填缓存(并设置 TTL)。
- 写:先更新数据库 → 再删除缓存(注意是「删除」而不是「更新缓存」)。
为什么写时是「删除」而不是「更新」?因为更新缓存有两个问题:① 并发写时,两次「更新缓存」的执行顺序可能与「更新数据库」的顺序相反,留下脏数据;② 若缓存值需要复杂计算,写频繁时会出现「写多读少却反复重建缓存」的浪费。删除是「懒加载」,下次读到时才重建。
残余风险与补偿手段:
| 风险 | 成因 | 补偿手段 |
|---|---|---|
| 删除缓存失败 | 网络抖动、Redis 宕机、应用重启 | ① 重试队列 / 消息队列(至少一次删除);② 订阅 binlog(Canal / Maxwell)由数据变更驱动删除 |
| 读请求回填旧值 | 读未命中 → 查库拿到旧值 → 写请求更新库并删缓存 → 读请求才把旧值写进缓存 | 延迟双删、给缓存设较短 TTL、或用 binlog 驱动删除 |
| 「先删缓存再更新库」的并发问题 | 删缓存之后、更新库之前的读请求把旧值回填 | 不要使用这个顺序(这正是选项 D 的错误方向) |
两种补偿方案对比:
| 方案 | 侵入性 | 可靠性 | 说明 |
|---|---|---|---|
| 重试 + MQ | 需业务代码配合(发消息、消费重试) | 较高(要求消息不丢、消费幂等) | 实现简单,适合中小规模 |
| 订阅 binlog(Canal) | 对业务代码无侵入 | 更高(以数据库提交为准,天然有序) | 需额外部署与运维 Canal 等中间件 |
理论边界:强一致做不到 —— 缓存与数据库是两个独立系统,无法用同一个事务包住;工程目标是最终一致,并把不一致窗口压到最短。
【记忆锚点】 「先库后缓存、删而不改、失败补偿(重试 / MQ / binlog)」——三件套缺一不可。
【易混对比】
- 删除缓存 vs 更新缓存:Cache Aside 选删除(懒加载、避免并发写乱序);只有「读极多、写极少」且能容忍短暂脏数据时才考虑更新缓存。
- 先删缓存再更新库 vs 先更新库再删缓存:前者在并发下会把旧值回填,风险更大;标准做法是后者。
- 缓存一致性 vs Cluster 重定向:
MOVED/ASK(补-29)是 Cluster 内部的请求转发,本题讲的是「库 - 缓存」之间的数据一致性,两者无关,是常见混淆点。 - 重试 + MQ vs binlog 订阅:前者有侵入、依赖消息可靠性;后者无侵入、以数据库为准,可靠性更高但需部署中间件。
- 换问法:若题干问「为什么要删缓存而不是更新缓存」,答案就是「避免并发写导致缓存与库顺序错乱,并避免写多读少时反复重建缓存」。
【自测】 用 Canal 订阅 MySQL binlog 来删除 Redis 缓存,相比「在业务代码里删缓存 + 失败进 MQ 重试」,主要优势是什么?
答:对业务代码无侵入,且以数据库提交的 binlog 为准(顺序可靠、不依赖业务侧是否漏发消息),一致性更强;代价是需要额外部署与运维 Canal 组件。与主库 R27、补-29 连考;互联网面试高频。
【选项误解】
- A 误解来源:「等 TTL 过期」窗口过长,业务读到脏数据。
- B 误解来源:DB 更新往往已提交,此时回滚代价极大甚至不可行(别的事务可能已读到新值)。
- C 正确项:重试+MQ 或订阅 binlog(Canal)异步删,保证最终一致。
- D 误解来源:方向反了。「先删缓存再更新库」在并发下容易把旧值回填进缓存。
【知识关联】
- 主库关联:R27(缓存与数据库一致性策略)、R05–R10(穿透/击穿/雪崩——不一致会放大故障面)、M14(binlog——Canal 的数据源)、M16(binlog 与 redo 一致)、补-29(Cluster 场景下删缓存要删对节点)。
- 国网/408:408 事务/日志可对照;互联网工程面试必考。
- 面试追问:① 为什么是删缓存不是更新缓存?→ 并发下更新顺序可能与 DB 相反;复杂值重建浪费。② 什么是延迟双删?→ 更新 DB 后删缓存,延迟几百毫秒再删一次,覆盖「回填旧值」竞态。③ Canal 原理?→ 伪装 MySQL slave 拉取 binlog,解析后驱动缓存删除/消息。
【拓展延伸】
- 变式问法:①「删除缓存失败最可靠补偿?」→ 重试/MQ/binlog 订阅;②「先库后缓存还是先缓存后库?」→ 标准 Cache Aside 是先库后删缓存;③「强一致可能吗?」→ 缓存与 DB 两系统难同一事务,目标最终一致。
- 完整竞态推导:
场景:读回填旧值
t1 读缓存未命中
t2 读 DB 得到旧值 V0
t3 写事务更新 DB 为 V1,并删缓存
t4 步骤 t2 的读请求才把 V0 写入缓存
→ 缓存长期停留在 V0(不一致)
补偿:
较短 TTL + 延迟双删 + binlog 驱动删除
Canal 路径:
DB commit → binlog → Canal → MQ/消费 → DEL cache
以 DB 为准,业务代码无侵入- 工程参数/命令:
# Canal 拉取 binlog 需要
log_bin=ON
binlog_format=ROW
binlog_row_image=FULL
# 应用侧
CacheAside: get → miss → load db → setex key ttl value
write: update db → del key(失败进重试队列)
# 延迟双删:del 后 sleep(500ms) 再 del
# Redis
SET key value EX 300
DEL key- 方案对比:
| 方案 | 侵入 | 可靠性 | 复杂度 |
|---|---|---|---|
| 忽略+TTL | 无 | 低 | 最低 |
| 同步删失败回滚 | 高 | 差(DB 已提交) | 高 |
| MQ 重试 | 中 | 较高 | 中 |
| Canal binlog | 低 | 高 | 需运维组件 |