Skip to content

补-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. 实体 → 表;属性 → 列(注意类型与约束)
  2. 1:1 联系 → 合并或一侧外键
  3. 1:n 联系 → n 端加外键
  4. m:n 联系 → 新建联系表,主键为两端键组合
  5. 规范化检查(至少到 3NF,必要时反范式)
  6. 定义用户视图(外模式)
  7. 与应用代码的模型映射(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 转表、定主键外键」→ 逻辑设计;③「建索引、选存储引擎」→ 物理设计;④「数据流图/数据字典」→ 需求分析。
  • 工程参数/命令:
text
逻辑设计产出:
  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 与日志锁的映射】

text
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;④「转账前后总额不变」→ 一致性(由其他特性共同保障)。
  • 工程参数/命令:
sql
-- 查看/设置隔离级别(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开始一次申请全部锁✓✓(预声明)看版本
顺序封锁按全局顺序申请✓✓—
严格 2PLX 锁持到结束✓✗✓
强严格 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 可串行化:
text
设并发调度 S 中各事务均服从 2PL
冲突操作次序由锁的相容性决定
扩展期:事务持续获得锁,其冲突操作必须排在其他持锁事务之后
收缩期:只放锁,不再产生新的「排到更前」的冲突约束
可证:调度的冲突图中无环 ⇔ 可串行化
(教材结论:2PL 是可串行化的充分条件,证明见冲突可串行化定理)
反例说明 2PL⇏无死锁:
  T1: lock-x(x) … lock-x(y)
  T2: lock-x(y) … lock-x(x)
  双方均遵守 2PL,但形成循环等待
  • 工程参数/命令:
sql
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

相容矩阵(行 = 已持有,列 = 新申请;✅ 相容可立即获准,❌ 互斥需等待):

已持有 \ 新申请SX
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?」→ 互斥等待。
  • 工程参数/命令:
sql
-- 悲观锁读/写
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;
  • 相容矩阵记忆:
text
        申请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 但无 COMMITUndo(用前像撤销)原子性

推导「为什么必须记录未提交事务的修改」:Undo 需要前像才能把数据改回原样。若日志里不写未提交事务的修改,宕机后就无从得知它改过什么,只能任由脏数据留在磁盘上 —— 原子性当场失效。这就是 B 项的致命错误。

检查点(Checkpoint)机制:系统定期把内存缓冲区的脏页刷回磁盘,并在日志中写入一条检查点记录,同时保存当前活跃事务表与日志位置。恢复时只需从最近一次检查点开始扫描(该点之前的修改已全部落盘),恢复时间从「扫描全量日志」降为「扫描一个区间」。

补充:检查点之后,检查点之前且已提交的事务日志可以被归档或清理,所以「节省空间」是它的副产品,而非定义 —— 这正是 C 项偷换概念的陷阱。

按故障类型选择恢复手段:

故障类型影响范围恢复手段
事务故障单个事务用日志 Undo 该事务
系统故障(宕机 / 断电)内存丢失、磁盘完好Redo 已提交 + Undo 未提交
介质故障(磁盘损坏)磁盘数据丢失备份 + 日志联合恢复

【知识点扩写·为什么必须记未提交的修改】

text
假设日志只记已提交事务:
  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 依据?」→ 前像;③「检查点主要目的?」→ 缩短恢复扫描范围,不是删日志;④「介质故障怎么恢复?」→ 备份 + 日志。
  • 完整恢复流程推导:
text
系统故障(断电、宕机,磁盘完好)恢复:
  1. 从 redo log 定位最近检查点
  2. 从检查点正向扫描日志,构造:
     - REDO-LIST:检查点之后已提交事务
     - UNDO-LIST:检查点之后未提交事务
  3. 对 REDO-LIST:按后像重做(幂等:可重复执行)
  4. 对 UNDO-LIST:按前像反向撤销
  5. 写恢复完成标记
事务故障:只需 Undo 该事务
介质故障:装载最近备份 → 再用日志重做到故障前
  • 工程参数/命令:
sql
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):假定冲突较少,读取时不加锁,提交更新时才校验数据是否被改过。

  • 版本号法:
sql
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(条件更新,兼具乐观思想)。
  • 工程参数/命令:
sql
-- 悲观
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;
  • 条件更新精简版(常被面试追问):
sql
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新读入的页、冷数据

两个关键机制,缺一不可:

  1. 新页先插入 old 区头部(不是 young 区头部),只有被再次访问才有资格晋升。
  2. 晋升有停留时间门槛:该页在 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_time vs innodb_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 区。
  • 完整机制推导:
text
标准 LRU 的问题:
  全表扫描瞬间读入大量页 → 全部插到 LRU 头部
  → 业务热点页被挤到尾部淘汰 → 扫描结束后缓存里全是「一次性页」
InnoDB 改进:
  LRU = young (5/8) + old (3/8)
  新页插入 old 区头部
  仅当在 old 区停留 > innodb_old_blocks_time 且再次被访问
  → 才晋升到 young 区头部
效果:
  扫描页在 old 区快速过期淘汰
  young 区热点页不受冲击
  • 工程参数/命令:
sql
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 的处理流程:

  1. 构建事务等待图(wait-for graph):顶点是事务,有向边表示「事务 T₁ 正在等待 T₂ 持有的锁」。
  2. 检测环路:innodb_deadlock_detect(MySQL 5.7.15 起可显式配置,默认 ON)开启时,每当有事务需要进入锁等待,就检查加入这条等待边后是否成环。
  3. 选择牺牲者:发现环路后,挑选回滚代价最小的事务(通常按修改行数、undo log 大小衡量)回滚,释放它持有的锁,其余事务得以继续。
  4. 报错返回:被牺牲的事务收到 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction,应用层必须重试。
  5. 超时兜底:若关闭死锁检测(innodb_deadlock_detect = OFF,可减少高并发下的检测开销),则退化为靠 innodb_lock_wait_timeout(默认 50 秒)超时释放。
机制触发条件后果是否默认
死锁检测事务等待图成环回滚牺牲者,其余事务继续✅ 默认开启
锁等待超时等待超过 innodb_lock_wait_timeout当前语句回滚并报错兜底(默认 50 秒)

预防死锁的四条实践:① 统一加锁顺序(如都按主键升序更新);② 缩短事务(快进快出,减少持锁时间);③ 给 WHERE 条件列建索引(避免行锁退化为锁全表);④ 必要时降低隔离级别(RR → RC 可减少间隙锁)。

【记忆锚点】 「死锁不等待,回滚代价最小的那个,其余照常走」——被牺牲的事务要由应用层重试。

【易混对比】

  • 死锁 vs 锁等待:锁等待是「暂时排不上队」(超时才报错),死锁是「互相卡死」(必须回滚一个)。
  • innodb_deadlock_detect vs innodb_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?」→ 捕获并重试;④「关闭检测的代价?」→ 死锁退化为长超时。
  • 完整流程推导:
text
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(统一升序)
  • 工程参数/命令:
sql
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,浮点只算科学」。

【易混对比】

  • DECIMAL vs FLOAT / DOUBLE:前者精确、运算略慢;后者近似、运算快。
  • DECIMAL vs 用 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 位。
  • 完整误差推导:
text
十进制 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 字节),运算精确
  • 工程参数/命令:
sql
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:

  1. 存储引擎用索引定位到所有 name LIKE 'a%' 的索引记录;
  2. 对每一条记录按主键回表,取出完整行;
  3. 把整行交给 Server 层,Server 层再判断 age = 20;
  4. 不满足的记录被丢弃 —— 回表白做了。

有 ICP(MySQL 5.6 引入,默认开启):

  1. 存储引擎定位到索引记录后,直接在索引上取出 age 列做判断;
  2. 只有同时满足 name LIKE 'a%' 与 age = 20 的记录才回表;
  3. 回表次数显著下降。
维度无 ICP有 ICP
过滤 age = 20 的位置Server 层存储引擎层
回表次数所有 name 命中的记录都回表仅两个条件都命中的记录回表
收益—减少回表、减少两层之间的交互

生效条件(三条同时满足):

  1. 访问类型为 range、ref、eq_ref、ref_or_null(即 EXPLAIN 的 type 列);
  2. 条件涉及联合索引中的后续列(如 (name, age) 里的 age),且该条件无法用于缩小索引扫描范围;
  3. 存储引擎支持 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 index vs Using index condition?→ 前者覆盖索引不回表;后者 ICP,仍可能回表但次数更少。② ICP 生效条件?→ range/ref/eq_ref 等 + 条件涉及联合索引后续列 + 引擎支持。③ 与 MRR 关系?→ Multi-Range Read 把随机回表整理成顺序,可与 ICP 叠加收益。

【拓展延伸】

  • 变式问法:①「ICP 的作用?」→ 引擎层提前过滤,减少回表;②「Extra: Using index condition 说明?」→ 启用了索引下推;③「如何减少回表?」→ 覆盖索引、ICP、延迟关联。
  • 完整执行对比推导:
text
表 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 回表)
  • 工程参数/命令:
sql
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 示意):
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(单机令牌桶)。
  • 固定窗口快速实现:
text
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 五大数据类型的能力边界:

类型底层结构(小规模 → 大规模)核心能力典型场景
Stringint / embstr → raw单值、计数(INCR)缓存对象、计数器、分布式锁
Hashlistpack → hashtable字段级读写(HSET / HGET)购物车、用户信息、对象缓存
Listlistpack → quicklist双端 O(1) 插入删除、有序消息队列、最新 N 条时间线
Setintset / listpack → hashtable去重、交并差(SINTER / SUNION)UV 去重、标签、共同好友
ZSetlistpack → skiplist + dict按 score 排序、范围查询、排名排行榜、延迟队列、带权重的 Top N

「精确去重计数」的三种解法与取舍:

方案关键命令精确性内存开销
SetSADD / SCARD✅ 精确与元素数量成正比,大 UV 场景很吃内存
HyperLogLogPFADD / PFCOUNT❌ 近似(标准误差约 0.81%)固定约 12 KB,极省
BitmapSETBIT / 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=到期时间戳)。
  • 工程参数/命令:
text
# 排行榜
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 的「数据类型」是对外接口,「底层编码」是内部实现 —— 同一类型在不同规模下用不同编码,以在「内存」与「性能」之间取舍。

数据类型小规模编码大规模编码相关配置项(默认值)
Stringint(整数值)/ embstr(7.x ≤44 字节)raw分界点:7.x=44 字节;Redis 8 起改为 key+值 ≤ 41 字节(实测判据见下表「版本实测」)
Hashlistpack(旧版 ziplist)hashtablehash-max-listpack-entries(512)、hash-max-listpack-value(64)
Listlistpack(单个 quicklist 节点)quicklist(多节点)list-max-listpack-size(默认 -2=按字节算,每节点 ≤8 KB,不是 128 个元素)
Setintset(全整数)/ listpackhashtableset-max-intset-entries(512)、set-max-listpack-entries(128)
ZSetlistpack(旧版 ziplist)skiplist + dictzset-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 不做反向转换,以避免频繁抖动)。

【知识点扩写·为何转换是单向的】

text
扩容方向:listpack/intset → hashtable/skiplist
  触发:元素个数或元素大小超过阈值
  原因:紧凑结构插入/查找退化为 O(N),内存连续搬移代价大
缩容方向:通常不自动逆转
  原因:避免「反复跨越阈值」导致频繁 realloc 与 rehash 抖动
  工程手段:导出再导入(DUMP/RESTORE),或删除 key 后重写
面试加分点:能指出「单向转换」以及 listpack 解决 ziplist 级联更新

【记忆锚点】 「小了挤一挤(紧凑编码省内存),大了就摊开(hashtable / skiplist 保性能);转换只往前、不回头」。

【易混对比】

  • ziplist vs listpack:ziplist 每个节点存「前一节点长度」,会导致级联更新(cascade update);listpack 只存自身长度,消除了级联更新,是 ziplist 的替代者。
  • embstr vs raw:都存字符串。embstr 把 RedisObject 与 SDS 放在同一块内存(一次分配、只读,适合短字符串),raw 分两次分配(适合长字符串或需修改的场景);分界点为 44 字节。
  • intset vs hashtable: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;④「删除后会自动变回紧凑吗?」→ 不会。
  • 工程参数/命令:
text
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
  • 阈值默认(常见):
类型配置默认
Hashhash-max-listpack-entries512
Hash valuehash-max-listpack-value64
ZSetzset-max-listpack-entries128
Set intsetset-max-intset-entries512
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 为例):

  1. 目标节点执行 CLUSTER SETSLOT <slot> IMPORTING <源节点 id>;
  2. 源节点执行 CLUSTER SETSLOT <slot> MIGRATING <目标节点 id>;
  3. 源节点对槽内每个 key 执行 MIGRATE,逐个搬走;
  4. 迁移完成后,新的槽归属在各节点间广播更新。

迁移期间源节点收到请求时:key 还在本节点 → 直接处理,不重定向;key 已迁到目标节点 → 返回 ASK(此时槽的归属还没变,客户端不能改映射);只有该槽已不再归属本节点(迁移完成、新归属已广播)才返回 MOVED。

Cluster 的扩容缩容正是通过在节点间迁移槽实现的,而不需要重算全部 key 的归属 —— 这是「槽」相对普通一致性哈希的改进点。

【记忆锚点】 「MOVED 是搬家搬完了(改地址本);ASK 是搬家搬到一半(这次先帮你转过去,地址本别动)」。

【易混对比】

  • MOVED vs ASK:判据是「槽的迁移是否完成」。槽归属已变更(迁移完成)→ MOVED + 更新映射;槽仍归本节点、只是这个 key 已迁到目标节点 → ASK + 不更新映射(key 还在源节点时由源节点直接处理,不重定向)。
  • ASK vs ASKING: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。
  • 工程参数/命令:
text
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
  • 完整迁移时序:
text
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 两系统难同一事务,目标最终一致。
  • 完整竞态推导:
text
场景:读回填旧值
  t1 读缓存未命中
  t2 读 DB 得到旧值 V0
  t3 写事务更新 DB 为 V1,并删缓存
  t4 步骤 t2 的读请求才把 V0 写入缓存
  → 缓存长期停留在 V0(不一致)
补偿:
  较短 TTL + 延迟双删 + binlog 驱动删除
Canal 路径:
  DB commit → binlog → Canal → MQ/消费 → DEL cache
  以 DB 为准,业务代码无侵入
  • 工程参数/命令:
text
# 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低高需运维组件

持续学习,持续积累。