{ "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 在不同崩溃类型下的数据差异。" } ] }