515acbcc7f
Deploy Examination / deploy (push) Successful in 4s
- mysql-acid (35): undo/redo log, WAL, 崩溃恢复三阶段, 两阶段提交 - mysql-transaction-isolation (35): 四隔离级别, 三异常, Read View, 快照读/当前读, next-key lock - redis-persistence (35): RDB/AOF/混合持久化, AOF 重写, vs binlog/redo log - 每子主题: single_choice×10 + fill_blank×10 + short_answer×10 + code_reading×5 - 全部通过 question.schema.json 校验
241 lines
15 KiB
JSON
241 lines
15 KiB
JSON
{
|
||
"topic": "mysql-acid",
|
||
"type": "code_reading",
|
||
"schema_version": "1.0.0",
|
||
"generated": "2026-09-07T00:00:00+08:00",
|
||
"questions": [
|
||
{
|
||
"id": "cr-001",
|
||
"type": "code_reading",
|
||
"difficulty": 3,
|
||
"tags": [
|
||
"崩溃恢复",
|
||
"redo log",
|
||
"undo log",
|
||
"两阶段提交"
|
||
],
|
||
"question": "请阅读以下 SQL 序列并思考崩溃点对 redo/undo 恢复的影响。假设数据库已开启 binlog,事务按两阶段提交协调 redo 与 binlog。",
|
||
"code": "-- 事务 T1:在多个节点同步写入插入数据,然后发生崩溃\nBEGIN;\nINSERT INTO orders(id, amount) VALUES (1001, 500);\nUPDATE orders SET amount = 900 WHERE id = 1001;\n-- ← 此处崩溃点:redo log 已完成 prepare,binlog 尚未写入完成\nCOMMIT;",
|
||
"language": "sql",
|
||
"source": null,
|
||
"related": [],
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"question": "在上述崩溃点(redo prepare 完成、binlog 未完整写入)恢复时,该事务 T1 应如何处理?",
|
||
"options": {
|
||
"A": "直接提交,因为 redo 已写入 prepare",
|
||
"B": "回滚(不提交),因为 binlog 中没有完整的该事务记录,两阶段提交判定其未成功提交",
|
||
"C": "无条件重做该事务的全部语句",
|
||
"D": "丢弃所有日志,保持崩溃前内存状态"
|
||
},
|
||
"answer": "B",
|
||
"explanation": "两阶段提交以“binlog 是否完整生成”作为提交裁决依据:若崩溃发生在 binlog 写入前(redo 仍停在 prepare / xa 状态未 commit),则恢复时判定该事务未成功提交,不重做,反而需要回滚其操作。所以 T1 被回滚,重做的是那些 binlog 完整的已提交事务。A、C 错误。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "恢复时,InnoDB 用哪个日志把 T1(未提交事务)的 INSERT 和 UPDATE 撤销回初始状态?请说明该日志记录的内容特征。",
|
||
"answer": "使用 undo log 回滚 T1。undo log 记录的是事务修改前的旧值(逻辑逆操作,例如把 amount 从 900 改回 500、删除 INSERT 插入的 id=1 行),回滚时按逆序执行这些逻辑操作,使数据恢复到事务开始前的一致状态。",
|
||
"explanation": "undo log 保存了修改前的旧值,用于把未提交事务的修改撤销。崩溃恢复在确认 T1 未提交后,沿 undo 链逆向执行,将 INSERT 的新行删除、把 amount 改回原值。重做已提交事务靠 redo log,而回滚未提交事务靠 undo log。",
|
||
"keywords": [
|
||
"undo log",
|
||
"回滚",
|
||
"旧值",
|
||
"逆序"
|
||
],
|
||
"scoring_rubric": "总分 4 分:答出用 undo log(1 分);说明记录旧值/逆操作的特征(2 分);说明回滚 INSERT/UPDATE 到原状(1 分)。"
|
||
}
|
||
],
|
||
"explanation": "本大题考察崩溃恢复时 redo/undo 的职责分工与两阶段提交裁决:binlog 完整则重做(redo),binlog 不完整则回滚(undo)。"
|
||
},
|
||
{
|
||
"id": "cr-002",
|
||
"type": "code_reading",
|
||
"difficulty": 3,
|
||
"tags": [
|
||
"Buffer Pool",
|
||
"脏页",
|
||
"刷盘",
|
||
"WAL",
|
||
"持久性"
|
||
],
|
||
"question": "请阅读下面描述 Buffer Pool 脏页刷盘流程的伪代码。",
|
||
"code": "-- 伪代码:处理一个批量 UPDATE 时脏页与 redo 的关系\nBUF_POOL.READ_PAGE(page_back; mem_page,P);\nmutmem.page.P: modify rows (rows 1..N0) -- 只改内存,不落盘\nredo.write(mem_page.P 的物理变更) + fsync() -- 先写日志(WAL)\nmark_page_dirty(P); -- 该页成为脏页\n-- 之后某时刻(checkpoint/后台刷脏)才做:\nflush_dirty_page(P) → 写入数据文件; clear_dirty_flag(P);",
|
||
"language": "text",
|
||
"source": null,
|
||
"related": [],
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"question": "根据上述流程,若在“flush_dirty_page(P)”执行前数据库崩溃,重启后最多丢失多少内容?",
|
||
"options": {
|
||
"A": "因为 redo 已先行持久化,数据不会丢失,重启后通过重放 redo 恢复该页修改",
|
||
"B": "脏页 P 的所有修改全部丢失,无法恢复",
|
||
"C": "丢失部分行,具体不可预测",
|
||
"D": "整个数据库损坏需要重建"
|
||
},
|
||
"answer": "A",
|
||
"explanation": "由于 redo log 在修改内存后立即(WAL)写盘并被 fsync 持久化,即使脏页 P 还未刷盘就崩溃,重启时也能通过扫描并重放 redo log 把该页的修改恢复,所以不会丢数据。这正是“先写日志再写数据”的意义所在,因此 A 正确。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "请解释:为什么脏页可以延迟批量刷盘而不破坏持久性?",
|
||
"answer": "因为 WAL 已经保证:任何数据页修改在刷到磁盘之前,对应的 redo log 已持续化(先写日志后写数据)。因此即使有大量脏页还未写回数据文件,崩溃时也能通过重放 redo log 重建这些页的最新状态。延迟批量刷盘(checkpoint/后台刷脏)既保持持久性,又把多次小的随机写合并成大的顺序批量 IO,显著提升写性能。",
|
||
"explanation": "持久性不依赖脏页及时刷盘,而依赖 redo log 先行持久化。批量刷盘牺牲的是脏页在内存的驻留时间,换取更高的 IO 效率,同时持久性完整保住。",
|
||
"keywords": [
|
||
"WAL",
|
||
"redo log 先落盘",
|
||
"重放恢复",
|
||
"批量刷盘",
|
||
"性能"
|
||
],
|
||
"scoring_rubric": "总分 4 分:指出 WAL/redo 先行持久化(2 分);说明崩溃可重放恢复(1 分);说明延迟批量刷盘提升 IO 性能且不破坏持久性(1 分)。"
|
||
}
|
||
],
|
||
"explanation": "本大题考察 Buffer Pool 脏页 + WAL 的组合机制:内存改页、日志先行、延迟刷盘、崩溃重放。"
|
||
},
|
||
{
|
||
"id": "cr-003",
|
||
"type": "code_reading",
|
||
"difficulty": 3,
|
||
"tags": [
|
||
"两阶段提交",
|
||
"redo log",
|
||
"binlog",
|
||
"一致性"
|
||
],
|
||
"question": "请阅读下面的两阶段提交流程描述。",
|
||
"code": "-- SQL 段:事务 T 内做一次 UPDATE\nBEGIN;\nUPDATE accounts SET bal = bal - 100 WHERE id = 1;\nCOMMIT;\n-- InnoDB 内部(binlog 与 redo)协调时序:\n 1) 写 redo( prepare 阶段) -- 暂不提交\n 2) 写 binlog(该事务的完整日志) \n 3) 写 redo( commit 标记) -- 事务正式提交",
|
||
"language": "text",
|
||
"source": null,
|
||
"related": [],
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"question": "若崩溃恰好发生在“步骤 2 写完 binlog 之后、步骤 3 写 redo commit 之前”,重启后该事务应如何?",
|
||
"options": {
|
||
"A": "直接回滚,因为 redo 未 commit",
|
||
"B": "判定为已提交并重做(因为 binlog 已完整,xid 存在),重新标记提交,保证主从一致",
|
||
"C": "既不提交也不回滚,永久悬挂",
|
||
"D": "丢弃该 UPDATE 的数据,但保留 binlog"
|
||
},
|
||
"answer": "B",
|
||
"explanation": "这是两阶段提交设计的典型场景:只要 binlog 完整写入,就认为事务可以提交。恢复时会发现该事务 redo 处于 prepare 但 binlog 已包含此 xid,于是判定提交成立,对 redo 进行提交(commit),并重做其修改。这样主库重放后的数据与从库(从 binlog 复制)一致。A、C、D 均不符合该裁决逻辑。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "两阶段提交如何保证崩溃后 redo log 与 binlog 反映的事务集合一致?",
|
||
"answer": "关键是以 binlog 作为一致性的“权威事务集合”。崩溃重启时扫描 redo 与 xa 事务状态:若某个事务在 binlog 中有完整记录,则判定为已提交(提交并重做);若 binlog 中无完整记录,则判定未提交并回滚。这样“已提交”的事务集合在 redo 和 binlog 中完全一致,主库重放后与从库 binlog 重放结果一致,从根本上消除两日志不一致导致的主从不一致或 PITR 偏差。",
|
||
"explanation": "两阶段提交通过在 redo 中区分 prepare/commit 阶段 + 以 binlog 为准的裁决,保证两日志在某时刻崩溃时仍能收敛到一致结果。",
|
||
"keywords": [
|
||
"以 binlog 为准",
|
||
"已提交才重做",
|
||
"未提交回滚",
|
||
"主从一致",
|
||
"xa 事务"
|
||
],
|
||
"scoring_rubric": "总分 5 分:解释以 binlog 是否完整作为提交裁决(2 分);说明完整则提交/重做(1 分);不完整则回滚(1 分);指出结果与理由主从/PPTR 一致(1 分)。"
|
||
}
|
||
],
|
||
"explanation": "本大题考察两阶段的“binlog 为准”裁决机制与崩溃后 redo/binlog 一致性保证。"
|
||
},
|
||
{
|
||
"id": "cr-004",
|
||
"type": "code_reading",
|
||
"difficulty": 3,
|
||
"tags": [
|
||
"WAL",
|
||
"写时序",
|
||
"redo log",
|
||
"持久性"
|
||
],
|
||
"question": "请阅读下面的时序描述并作答。",
|
||
"code": "-- 单事务提交时 InnoDB 的写时序(同步点)\n(1) 在 Buffer Pool 修改内存页 P(标记脏页)\n(2) 将该页的物理变更追加到 redo log buffer\n(3) 在事务提交时:将 redo buffer 写入 OS 缓存并 fsync 到磁盘\n(4) 然后根据 innodb_flush_log_at_trx_commit 取值决定何时 fsync\n(5) 数据页 P 仍留在内存,等待后续 checkpoint 刷盘",
|
||
"language": "text",
|
||
"source": null,
|
||
"related": [],
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"question": "下列哪一条关于上述写时序的描述是正确的?",
|
||
"options": {
|
||
"A": "先写数据页 P 到磁盘,再写 redo log,此顺序是 WAL",
|
||
"B": "数据页 P 同步立即刷盘,redo log 最后才写",
|
||
"C": "先记录 redo log 并持久化(WAL),数据页 P 可延迟刷盘,从而保证崩溃可恢复",
|
||
"D": "redo log 与数据页 P 必须同时在一个事务中写盘"
|
||
},
|
||
"answer": "C",
|
||
"explanation": "该时序体现了正确的 WAL:redo log 在提交时持久化(步骤 3),数据页 P 在步骤 5 才延迟刷盘。只要 redo log 先落盘并安全,无论脏页何时刷,崩溃都可通过重放 redo 恢复。A 顺序说反;B、D 与延迟刷盘的 WAL 机制不符。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "为什么事务提交时只需把 redo log 落盘(而不必把对应数据页落盘),却仍能满足提交后的持久性?",
|
||
"answer": "因为提交语义建立在 redo log 的持久化之上:只要 redo 记录已通过 fsync 写在磁盘,即使数据页尚未刷盘,一旦崩溃,InnoDB 重启后也能通过扫描并重放 redo log,把该页的修改恢复到数据文件,从而让已提交(已落 redo)的事务修改永不丢失。数据页落盘可以异步延迟执行,因此提交时只需 redo 落盘,既保证一次成持久性又避免每次 commit 都刷一整页的低效 IO。",
|
||
"explanation": "持久性依赖 redo 的持久化而非脏页的当前状态,因此提交时可只 fsync redo,干净保证已提交数据可恢复。",
|
||
"keywords": [
|
||
"redo 持久化",
|
||
"重放恢复",
|
||
"脏页延迟刷",
|
||
"提交即持久"
|
||
],
|
||
"scoring_rubric": "总分 4 分:指出 redo 已持久化(1 分);说明崩溃时重放 redo 可恢复该页(2 分);说明避免低效/延迟刷盘(1 分)合计 4 分。"
|
||
}
|
||
],
|
||
"explanation": "本大题考察正确的 WAL 写时序:redo 先行持久化,脏页延迟刷盘,提交即持久依赖 redo。"
|
||
},
|
||
{
|
||
"id": "cr-005",
|
||
"type": "code_reading",
|
||
"difficulty": 2,
|
||
"tags": [
|
||
"innodb_flush_log_at_trx_commit",
|
||
"持久性",
|
||
"性能",
|
||
"配置"
|
||
],
|
||
"question": "请阅读以下配置场景并作答。",
|
||
"code": "-- Deep 场景(持续高压写入的 ETL 批量导入)\nSET GLOBAL innodb_flush_log_at_trx_commit = 0;\n-- 期间执行大量 INSERT/UPDATE,然后 MySQL 进程突然崩溃(未正常关机)",
|
||
"language": "sql",
|
||
"source": null,
|
||
"related": [],
|
||
"sub_questions": [
|
||
{
|
||
"index": 1,
|
||
"type": "single_choice",
|
||
"question": "崩溃重启后,最近约 1 秒内已提交的事务可能出现什么情况?",
|
||
"options": {
|
||
"A": "所有已提交事务完整保留,一个不丢",
|
||
"B": "最近 1 秒内已提交但尚未刷盘的事务可能丢失(被回滚/未恢复),持久性被削弱",
|
||
"C": "全部数据丢失,包括很久前已提交的",
|
||
"D": "事务数据完全不受影响,只影响查询性能"
|
||
},
|
||
"answer": "B",
|
||
"explanation": "innodb_flush_log_at_trx_commit=0 表示每 1 秒才刷一次 redo log 到磁盘(进程崩溃时未触发的同步点会丢接近 1 秒内的 redo)。因此最近约 1 秒内已“提交”但 redo 尚未落盘的事务,若进程崩溃,将无法通过 redo 恢复,从而缺失(属于主动牺牲持久性换取更高写入吞吐的场景)。A、C、D 与该配置下的行为不符。"
|
||
},
|
||
{
|
||
"index": 2,
|
||
"type": "short_answer",
|
||
"question": "若将该参数改为 2,在“MySQL 进程崩溃”与“操作系统崩溃”两种情形下,已提交事务丢失情况分别如何?",
|
||
"answer": "取值 2 时,redo 每次提交只写入(write,不 fsync)OS 缓存(Page Cache)。因此:MySQL 进程崩溃时,因 OS 仍存活,缓存中的 redo 不丢,已提交事务不丢失;但若是操作系统(或断电等导致缓存丢失)崩溃,则 OS 缓存中的 redo 也可能丢失近 1 秒已提交数据;若要绝对不丢则必须 set 1(每次 fsync)。",
|
||
"explanation": "取值 2 写入 OS 缓存的语义:进程崩溃时 OS 缓存完好,数据不丢;OS 崩溃时缓存丢失可能丢接近 1 秒数据,严格持久性需 1。",
|
||
"keywords": [
|
||
"OS 缓存",
|
||
"进程崩溃不丢",
|
||
"OS 崩溃可能丢",
|
||
"取值 1 严格"
|
||
],
|
||
"scoring_rubric": "总分 4 分:说明 2 只写 OS 缓存不 fsync(1 分);进程崩溃时不丢(1 分);OS 崩溃可能丢近 1 秒(1 分);严格需要=1(1 分)。"
|
||
}
|
||
],
|
||
"explanation": "本大题考察 innodb_flush_log_at_trx_commit 三取值的持久性与崩溃语义,特别对比 0、2 在不同崩溃类型下的数据差异。"
|
||
}
|
||
]
|
||
} |