From 515acbcc7fbe2113c1f02bceed4b6ad2c86a95ec Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 7 Sep 2026 19:17:38 +0800 Subject: [PATCH] feat: add database topic group with 105 questions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 校验 --- topics/database/mysql-acid/code_reading.json | 241 ++++++++++++++++ topics/database/mysql-acid/fill_blank.json | 198 +++++++++++++ topics/database/mysql-acid/meta.json | 50 ++++ topics/database/mysql-acid/short_answer.json | 241 ++++++++++++++++ topics/database/mysql-acid/single_choice.json | 229 +++++++++++++++ .../code_reading.json | 257 +++++++++++++++++ .../fill_blank.json | 201 ++++++++++++++ .../mysql-transaction-isolation/meta.json | 41 +++ .../short_answer.json | 213 ++++++++++++++ .../single_choice.json | 211 ++++++++++++++ .../redis-persistence/code_reading.json | 254 +++++++++++++++++ .../redis-persistence/fill_blank.json | 215 +++++++++++++++ topics/database/redis-persistence/meta.json | 57 ++++ .../redis-persistence/short_answer.json | 261 ++++++++++++++++++ .../redis-persistence/single_choice.json | 229 +++++++++++++++ topics/index.json | 54 +++- 16 files changed, 2951 insertions(+), 1 deletion(-) create mode 100644 topics/database/mysql-acid/code_reading.json create mode 100644 topics/database/mysql-acid/fill_blank.json create mode 100644 topics/database/mysql-acid/meta.json create mode 100644 topics/database/mysql-acid/short_answer.json create mode 100644 topics/database/mysql-acid/single_choice.json create mode 100644 topics/database/mysql-transaction-isolation/code_reading.json create mode 100644 topics/database/mysql-transaction-isolation/fill_blank.json create mode 100644 topics/database/mysql-transaction-isolation/meta.json create mode 100644 topics/database/mysql-transaction-isolation/short_answer.json create mode 100644 topics/database/mysql-transaction-isolation/single_choice.json create mode 100644 topics/database/redis-persistence/code_reading.json create mode 100644 topics/database/redis-persistence/fill_blank.json create mode 100644 topics/database/redis-persistence/meta.json create mode 100644 topics/database/redis-persistence/short_answer.json create mode 100644 topics/database/redis-persistence/single_choice.json diff --git a/topics/database/mysql-acid/code_reading.json b/topics/database/mysql-acid/code_reading.json new file mode 100644 index 0000000..7ae916f --- /dev/null +++ b/topics/database/mysql-acid/code_reading.json @@ -0,0 +1,241 @@ +{ + "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 在不同崩溃类型下的数据差异。" + } + ] +} \ No newline at end of file diff --git a/topics/database/mysql-acid/fill_blank.json b/topics/database/mysql-acid/fill_blank.json new file mode 100644 index 0000000..f27a648 --- /dev/null +++ b/topics/database/mysql-acid/fill_blank.json @@ -0,0 +1,198 @@ +{ + "topic": "mysql-acid", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 1, + "tags": [ + "原子性", + "undo log", + "InnoDB" + ], + "question": "InnoDB 保证事务原子性依赖的日志是 ____,它记录的是修改前的旧值,回滚时据此恢复数据。", + "answer": [ + "undo log" + ], + "answer_rule": "any", + "explanation": "undo log 记录的是事务修改前的旧值(逻辑逆操作)。当事务回滚或崩溃恢复时,InnoDB 依据 undo log 将数据恢复到事务开始前的状态,从而保证原子性(要么全部提交要么全部回滚)。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 1, + "tags": [ + "持久性", + "WAL", + "redo log" + ], + "question": "InnoDB 保证持久性采用 Write-Ahead Logging 策略,即在数据页落盘前必须先将对应的 ____ 日志写入磁盘。", + "answer": [ + "redo log" + ], + "answer_rule": "any", + "explanation": "WAL 的核心是“写日志先行”:任何数据页修改在刷入磁盘之前,必须先把对应的 redo log 记录写入磁盘。这样一旦崩溃,通过重放 redo log 即可恢复未落盘的已提交修改,从而保证持久性。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "redo log", + "物理日志", + "环形缓冲", + "InnoDB" + ], + "question": "InnoDB 的 redo log 在逻辑上是固定大小的环形缓冲,写入是循环的,当写满时会覆盖最 ____ 的日志。", + "answer": [ + "旧" + ], + "answer_rule": "any", + "explanation": "redo log 由若干固定大小的文件组成环形结构,物理日志循环写入,当文件写满会回绕覆盖最旧(最早写入)的日志记录。因此 redo log 并不记录全部历史,而只保留最近一段时间的重做记录。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "redo log", + "物理日志", + "undo log", + "逻辑日志" + ], + "question": "redo log 记录的是对数据页字节修改的____日志(物理层),而 undo log 记录的是如何“改回旧值”的____日志(逻辑层)。", + "answer": [ + "物理", + "逻辑" + ], + "answer_rule": "ordered", + "explanation": "redo log 属于物理日志,描述对哪个页的哪个位置的字节进行了什么修改,用于崩溃后重放;undo log 属于逻辑日志,记录的是把数据改回旧状态所需的逆操作逻辑。两个空位必须按顺序填“物理 / 逻辑”,所以使用 ordered 规则。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "MVCC", + "undo log", + "隔离性" + ], + "question": "InnoDB 的 MVCC 通过 undo log 维护行的多____链,配合事务的 ____ 快照决定当前读可见的版本,从而在不加锁的情况下实现一致性的快照读。", + "answer": [ + "版本", + "ReadView" + ], + "answer_rule": "ordered", + "explanation": "InnoDB 通过 undo log 维护行的多版本链,每次更新原记录时旧版本被放入版本链。一致性读(快照读)通过 ReadView(一个事务范围内的可见性判断结构)沿版本链找到当前事务可见的版本,实现高并发的非锁定读。两空位按顺序填“版本 / ReadView”。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "崩溃恢复", + "redo log", + "undo log" + ], + "question": "InnoDB 崩溃恢复的三阶段为:一、扫描 ____ 确定需要恢复的范围;二、对其中的已提交事务进行 ____;三、对未提交事务用 ____ 进行回滚。", + "answer": [ + "redo log", + "重做", + "undo log" + ], + "answer_rule": "ordered", + "explanation": "崩溃恢复流程:先扫描 redo log 确定恢复起点;然后重做(应用)崩溃前已提交但未刷盘的事务,使其修改落到数据页;最后根据两阶段提交状态,对未提交事务用 undo log 回滚,恢复到崩溃前的一致状态。三个空位需按顺序作答。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "binlog", + "redo log", + "两阶段提交" + ], + "question": "为协调 InnoDB 与 binlog 的一致性,MySQL 对两者采用____阶段提交:先将 redo log 写入____(prepare)阶段,再写 binlog,最后把 redo 标记为 commit。", + "answer": [ + "两", + "prepare" + ], + "answer_rule": "ordered", + "explanation": "MySQL 对 redo log 与 binlog 用两阶段提交(2PC / InnoDB&binlog 内部协调):第一步把 redo log 写入 prepare 阶段(此刻事务处于预提交),第二步写 binlog,第三步将 redo 标记为 commit。崩溃重启时依据 binlog 是否完整来决定事务是提交还是回滚,从而保证两者一致。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "innodb_flush_log_at_trx_commit", + "持久性" + ], + "question": "innodb_flush_log_at_trx_commit=1 时,每次事务提交都会调用 ____ 强制把 redo log 刷到磁盘,保证最强的持久性(也最浪费 IO)。", + "answer": [ + "fsync" + ], + "answer_rule": "any", + "explanation": "取值 1 意味着每次提交时 InnoDB 都调用 fsync 把 redo log 从 OS 缓存强制刷入磁盘,确保提交的事务真正持久化,因此符合严格 ACID 的持久性要求,但会带来较多磁盘同步 IO。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "redo log", + "binlog", + "主从复制", + "审计" + ], + "question": "binlog 是 MySQL ____ 层的逻辑日志,主要用于主从复制、基于时间点恢复(PITR)和 ____;而 redo log 是 InnoDB 引擎层用于崩溃恢复的日志。", + "answer": [ + "Server", + "审计" + ], + "answer_rule": "any", + "explanation": "binlog 由 MySQL 的 Server 层记录逻辑变更,其主要用途包括主从复制(从库重放 binlog)、基于时间点恢复(PITR)以及审计。而 redo log 属于 InnoDB 存储引擎层,只用于崩溃恢复时重做已提交事务。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "Buffer Pool", + "脏页", + "刷盘", + "WAL" + ], + "question": "Buffer Pool 中被修改而未刷盘的数据页称为 ____ 页,InnoDB 会延迟到合适时机(如 CHECKPOINT)才批量 ____ 并将其落盘;由于 redo log 已先行持久化,即使这期间崩溃也能通过重放恢复。", + "answer": [ + "脏", + "刷盘" + ], + "answer_rule": "ordered", + "explanation": "Buffer Pool 中已修改但尚未写回磁盘的页称为脏页。InnoDB 通常在检查点(checkpoint)或后台刷脏线程等时机批量刷盘,而不是每次修改都立即落盘。因为 redo log 已先持久化,脏页即便在刷盘前崩溃,也能通过重放 redo 恢复,这正是 WAL + 延迟刷盘配合的高效设计。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/database/mysql-acid/meta.json b/topics/database/mysql-acid/meta.json new file mode 100644 index 0000000..dfb7017 --- /dev/null +++ b/topics/database/mysql-acid/meta.json @@ -0,0 +1,50 @@ +{ + "slug": "mysql-acid", + "name": "MySQL ACID 实现机制", + "description": "InnoDB 如何用日志与锁实现事务的原子性/一致性/隔离性/持久性:undo log、redo log、WAL、MVCC、锁", + "tags": [ + "ACID", + "InnoDB", + "MVCC", + "WAL", + "binlog", + "innodb_flush_log_at_trx_commit", + "next-key lock", + "redo log", + "undo log", + "一致性", + "两阶段提交", + "原子性", + "崩溃恢复", + "快照读", + "性能", + "持久性", + "物理日志", + "约束", + "行锁", + "逻辑日志", + "间隙锁", + "隔离性" + ], + "difficulty_range": [ + 2, + 4 + ], + "schema_version": "1.0.0", + "updated": "2026-09-07", + "question_files": [ + "single_choice", + "fill_blank", + "code_reading", + "short_answer" + ], + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "fill_blank": 10, + "code_reading": 5, + "short_answer": 10 + } + } +} diff --git a/topics/database/mysql-acid/short_answer.json b/topics/database/mysql-acid/short_answer.json new file mode 100644 index 0000000..204a146 --- /dev/null +++ b/topics/database/mysql-acid/short_answer.json @@ -0,0 +1,241 @@ +{ + "topic": "mysql-acid", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "ACID", + "原子性", + "undo log", + "InnoDB" + ], + "question": "解释 InnoDB 如何用 undo log 实现事务的原子性(回滚)。", + "answer": "InnoDB 在每个事务执行写操作时,先把旧值(修改前的数据)写入 undo log。若事务需要回滚或崩溃恢复时确定未提交,InnoDB 依据 undo log 中记录的旧值做逆操作:INSERT 对应 DELETE 反操作、DELETE 对应重新插入、UPDATE 则把新旧值互换改回,从而把数据恢复到事务开始前的状态。整个事务要么全部提交要么全部回滚,即为原子性。", + "keywords": [ + "undo log", + "旧值", + "逆序回滚", + "原子性" + ], + "scoring_rubric": "总分 4 分:说明修改前写 undo log(2 分);说明用旧值逆序回滚(2 分)。", + "explanation": "核心是“记录旧值 + 逆操作回滚”。答出 undo log 记录旧值、支持把数据恢复到事务前状态即可得分。", + "source": null, + "related": [] + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "持久性", + "redo log", + "WAL", + "InnoDB" + ], + "question": "什么是 Write-Ahead Logging(WAL)?它在 MySQL InnoDB 中如何保证持久性?", + "answer": "WAL 指“先写日志再写数据”:任何数据页的修改在落盘之前,必须先把对应的 redo log 记录写入磁盘并持久化。InnoDB 修改 Buffer Pool 中的页时会先向 redo log 追加物理变更;事务提交时确保该 redo 已写入磁盘(fsync,或按 innodb_flush_log_at_trx_commit 决定)。若在数据页落盘前崩溃,重启时通过扫描并重放 redo log,把已提交事务的修改恢复到数据文件,因此已提交数据不会丢失,从而保证持久性。", + "keywords": [ + "WAL", + "先写日志再写数据", + "redo log", + "重放恢复", + "持久性" + ], + "scoring_rubric": "总分 4 分:给出 WAL 定义“先写日志后写数据”(2 分);说明崩溃重放 redo 恢复(1 分);结合 InnoDB 描述(1 分)。", + "explanation": "先陈述 WAL 的顺序要点,再联系 redo log 在这次重放(redo)与 fsync 的细节,符合本主题核心理解。", + "source": null, + "related": [] + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "redo log", + "物理日志", + "undo log", + "逻辑日志" + ], + "question": "对比 redo log 与 undo log 在记录内容(物理/逻辑)和作用上的区别。", + "answer": "两者差异如下:1)记录内容:redo log 是物理日志,记录了对哪个数据页哪一处的字节所做的实际修改;undo log 是逻辑日志,记录如何把一行数据“改回旧值”的逆操作。2)作用方向:redo 用于崩溃后重做(REDO)已提交事务,保证持久性;undo 用于回滚未提交事务(或 MVCC 提供历史版本)保证原子性与隔离读。redo 是能重做的物理重放,undo 是可逆回滚的逻辑逆操作。", + "keywords": [ + "redo 物理", + "undo 逻辑", + "重做已提交", + "回滚未提交", + "崩溃恢复" + ], + "scoring_rubric": "总分 4 分:redo 物理日志(1 分);undo 逻辑日志(1 分);redo 重做已提交(1 分);undo 回滚未提交(1 分)。", + "explanation": "分别从“物理 vs 逻辑”“重做 vs 回滚”两个维度对比,即可覆盖核心差异。", + "source": null, + "related": [] + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "崩溃恢复", + "redo log", + "undo log", + "InnoDB" + ], + "question": "详细描述 InnoDB 崩溃恢复的三阶段流程。", + "answer": "第一阶段(扫描 redo log):从 redo 中确定本次恢复需要处理的 lSN 范围与 checkpoint 位置,找出需要重做的日志段。第二阶段(重做已提交事务):按 redo log 将崩溃前已提交但尚未刷盘到数据文件的数据页修改重新应用(redo ),使这些已提交的修改得到恢复。第三阶段(回滚未提交事务):通过两阶段提交记录的 xid 识别哪些事务未在 binlog 中完整提交,利用 undo log 把这些未提交事务的执行进行回滚,最终使数据库恢复到崩溃前一致、没有丢失已提交数据且无未提交改动残留的状态。", + "keywords": [ + "扫描 redo", + "重做已提交", + "回滚未提交", + "两阶段提交", + "checkpoint" + ], + "scoring_rubric": "总分 4 分:扫描 redo 确定范围(1 分);重做已提交事务(2 分);用 undo 回滚未提交(1 分)。", + "explanation": "按顺序答出重做/回滚的职责与对象判断(以两阶段提交/是否已提交为依据)即可。", + "source": null, + "related": [] + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "隔离性", + "MVCC", + "undo log", + "快照读" + ], + "question": "MVCC 是什么?InnoDB 使用它如何提升并发隔离性?", + "answer": "MVCC(多版本并发控制)是 InnoDB 提供一致性非锁定读的机制:通过 undo log 维护每一行的多版本链。当普通 SELECT(快照读)执行时,InnoDB 依据事务的 ReadView(可见性判断)沿版本链找到当前事务可见版本,不需要加共享锁。这样多个读事务与写事务可并发执行,读不被写阻塞,从而在维持隔离性的前提下显著提升并发吞吐。", + "keywords": [ + "MVCC", + "undo log 版本链", + "ReadView", + "一致性读", + "非锁读" + ], + "scoring_rubric": "总分 3 分:给出 MVCC 定义(1 分);说明 undo 多版本 + ReadView(1 分);读不加锁、提升并发(1 分)。", + "explanation": "抓住“多版本链 + ReadView + 读不加锁”即可说明 MVCC 是如何提升并发也保隔离的。", + "source": null, + "related": [] + }, + { + "id": "sa-006", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "隔离性", + "行锁", + "间隙锁", + "next-key lock" + ], + "question": "在可重复读(RR)隔离级别下,InnoDB 的 next-key lock(临键锁)如何防止幻读?", + "answer": "RR 下 InnoDB 对索引范围/查找会加 next-key lock(由行锁 + 间隙锁组成的左开右闭区间锁)。它既锁住命中的记录(行锁),又锁住记录之间的间隙(gap),阻止其他事务在该扫描区间内 INSERT 新行。因为在该被锁定的范围里不可能再插入新记录,当前事务多次查询同一范围结果一致,从而防止了幻读。", + "keywords": [ + "next-key lock", + "gap lock", + "锁定区间", + "防幻读", + "RR" + ], + "scoring_rubric": "总分 3 分:指出 next-key = 行锁 + 间隙锁(1 分);锁区间阻止插入(1 分);讲到幻读消除(1 分)。", + "explanation": "从一个锁出发:锁间区使别人无法插入,故范围查询结果稳定,防幻读。", + "source": null, + "related": [] + }, + { + "id": "sa-007", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "两阶段提交", + "redo log", + "binlog", + "一致性" + ], + "question": "为什么 MySQL 需要用两阶段提交来协调 redo log 与 binlog?", + "answer": "因为同一条事务的修改既要写 redo log(用于崩溃恢复)又要写 binlog(用于复制与恢复),若没协调,崩溃可能发生在一个日志已写另一个未写的时刻,导致主库重放后与从库/恢复点不一致。两阶段提交通过 prepare(redo 先 prepare)+写 binlog+ commit(redo 标记)的划分,并在恢复时以“binlog 是否完整作为事务是否提交”的裁决,保证 redo 与 binlog 的已提交事务集合完全一致,杜绝主从数据不一致与 PITR 偏差。", + "keywords": [ + "两阶段提交", + "prepare", + "binlog 为准", + "主从一致", + "xa" + ], + "scoring_rubric": "总分 4 分:指出两份日志都需要(1 分);说明子过程 prepare/binlog/commit(1 分);说明以 binlog 为准裁决提交(2 分)。", + "explanation": "核心是“防止在日志中间崩溃造成两份日志事务集不一致”,并以 binlog 为裁决者。", + "source": null, + "related": [] + }, + { + "id": "sa-008", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "redo log", + "binlog" + ], + "question": "redo log 与 binlog 各自的应用场景和作用是什么?", + "answer": "redo log 是 InnoDB 引擎层专用于崩溃恢复的重做日志(物理记录页修改);binlog 是 MySQL Server 层的逻辑日志,记录事务的逻辑变更,主要场景为主从复制(从库重放 binlog)、基于时间点/位置的恢复 PITR 及审计。redo 在崩溃恢复时让 InnoDB 持久化已提交数据,binlog 面向整个实例的外部同步与运维。", + "keywords": [ + "redo 崩溃恢复", + "binlog 复制", + "PITR", + "审计" + ], + "scoring_rubric": "总分 3 分:redo 崩溃重做(1 分);binlog 复制 PITR(1 分);指出层与外部区别(1 分)。", + "explanation": "从时间/层和用途三方面对比即可。", + "source": null, + "related": [] + }, + { + "id": "sa-009", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "一致性", + "约束", + "ACID" + ], + "question": "解释在 InnoDB 中一致性(Consistency)是如何实现的,并说明它与 A、I、D 的关系。", + "answer": "一致性指事务保证数据从一致状态到一致的状态。InnoDB 通过约束(主键、唯一、非空、外键、CHECK 等)保证提交的数据满足完整性,同时一致性依赖于另三者:若原子性被破坏会造成部分写入,隔离性破坏可能读到中间态,持久性破坏会导致状态回退。只有当原子性(全提交或全回滚)、隔离性(并发不受互扰)、持久性(提交可恢复)三者共同成立,配合约束与业务逻辑,才能把已提交事务改变成一个整体一致的状态。", + "keywords": [ + "约束", + "原子性", + "隔离性", + "持久性", + "一致状态" + ], + "scoring_rubric": "总分 4 分:提到约束(1 分);A/I/D 各自如何维护一致(2 分);说明三者共同保证(1 分)。", + "explanation": "一致性不是一个独立机制,而是约束 + A/I/D 的协作者;答出三者影响一致性的链路即好。", + "source": null, + "related": [] + }, + { + "id": "sa-010", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "innodb_flush_log_at_trx_commit", + "持久性", + "性能" + ], + "question": "对比 innodb_flush_log_at_trx_commit 取 (0/1/2) 三种值在持久性与性能上的取舍。", + "answer": "0:每秒刷一次 redo log 盘,性能直高但进程崩溃会丢近 1 秒已提交事务;1:每次提交都 fsync 刷盘,持久性最强、最安全,但磁盘同步 IO 开销大、提交延迟高;2:每次提交把 redo 写至 OS 缓存(write,不 fsync),MySQL 进程崩溃时数据不丢,但 OS 崩溃/断电可能丢近 1 秒数据,性能比起 1 明显更高、比 0 稍低。生产 Server 建议设置为 1(严格 ACID),对可容忍少量丢亦可 2/0 提升吞吐。", + "keywords": [ + "0 每秒刷盘", + "1 每次 fsync", + "2 OS 缓存", + "持久性 vs 性能" + ], + "scoring_rubric": "总分 4 分:0 语义与丢数据(1 分);1 语义持久性最强(1 分);2 写 OS 缓存语义(1 分);指出取舍与推荐(1 分)。", + "explanation": "三个取值逐一说明“何时同步 fsync”即可,再补充其与几种崩溃类型的对应关系。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/database/mysql-acid/single_choice.json b/topics/database/mysql-acid/single_choice.json new file mode 100644 index 0000000..24aea1a --- /dev/null +++ b/topics/database/mysql-acid/single_choice.json @@ -0,0 +1,229 @@ +{ + "topic": "mysql-acid", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 1, + "tags": [ + "ACID", + "原子性", + "undo log", + "InnoDB" + ], + "question": "InnoDB 中,事务回滚时依赖哪种日志来恢复旧值,从而保证 ACID 中的原子性?", + "options": { + "A": "redo log", + "B": "binlog", + "C": "undo log", + "D": "slow query log" + }, + "answer": "C", + "explanation": "undo log 记录的是事务修改前的旧值(逻辑逆操作),当事务回滚或崩溃恢复时用它把数据恢复原状,保证原子性(要么全部执行要么全部回滚)。redo log 用于崩溃恢复时重做已提交事务,保证持久性;binlog 是 Server 层的逻辑日志,用于复制和 PITR;slow query log 只是性能诊断日志,与原子性无关。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 1, + "tags": [ + "持久性", + "redo log", + "WAL", + "InnoDB" + ], + "question": "MySQL InnoDB 的 WAL(Write-Ahead Logging)核心思想是:", + "options": { + "A": "先写数据文件再写日志文件", + "B": "先写 redo log,再刷数据页,崩溃后通过 redo 重放保证持久性", + "C": "只在内存中修改数据,从不落盘", + "D": "每次修改直接覆盖数据文件" + }, + "answer": "B", + "explanation": "WAL 要求任何数据页修改落盘之前,先把对应的 redo log 记录写入磁盘并保证其持久化(fsync 或满足配置条件)。这样即便数据页还没刷盘就发生崩溃,重启后也能通过重放 redo log 恢复已提交的数据,从而保证持久性。A、D 违背 WAL 顺序;C 显然错误,数据最终要落盘。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "redo log", + "物理日志", + "环形缓冲", + "InnoDB" + ], + "question": "关于 InnoDB redo log 的说法,正确的是:", + "options": { + "A": "redo log 是固定大小的循环写入的物理日志,写满后会覆盖最旧的日志", + "B": "redo log 可以无限增长,永远不会被覆盖", + "C": "redo log 记录的是 SQL 语句本身(逻辑日志)", + "D": "redo log 存储在系统变量中,不落盘" + }, + "answer": "A", + "explanation": "InnoDB 的 redo log 在逻辑上是一个环形缓冲(固定大小,由 innodb_log_file_size 决定),写入是循环的,写满时会覆盖最旧的日志。它是物理日志(记录对哪个页哪一处的实际字节修改),而非逻辑日志。日志需要持久化到磁盘才能发挥作用,所以 C、D 错误。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "redo log", + "binlog", + "两阶段提交", + "InnoDB" + ], + "question": "Redo log 与 binlog 的主要区别在于:", + "options": { + "A": "两者完全相同,只是名字不同", + "B": "redo log 是 InnoDB 引擎层用于崩溃恢复,binlog 是 Server 层用于主从复制、PITR 和审计", + "C": "binlog 用于崩溃恢复,redo log 用于主从复制", + "D": "redo log 记录逻辑 SQL,binlog 记录物理页修改" + }, + "answer": "B", + "explanation": "redo log 是 InnoDB 特有的物理重做日志,只在崩溃恢复时重放已提交事务;binlog 是 MySQL Server 层的逻辑日志(记录对数据库的逻辑改动),服务于主从复制、基于时间点的恢复(PITR)和审计。C 把职责颠倒了。redo log 是物理的、binlog 是逻辑的,所以 D 也错。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "两阶段提交", + "redo log", + "binlog", + "一致性" + ], + "question": "在 InnoDB 与 binlog 之间使用两阶段提交(2PC)的主要目的是:", + "options": { + "A": "提升写入性能,减少磁盘 IO", + "B": "保证 redo log 与 binlog 的一致性,避免崩溃后主从数据不一致", + "C": "加快崩溃恢复速度", + "D": "减少内存占用" + }, + "answer": "B", + "explanation": "同一事务会同时写 redo log 和 binlog。两阶段提交把写入过程分成 prepare(redo 写 prepare 阶段)和 commit,崩溃恢复时以 binlog 是否完整生成(xa 事务状态)为准来决定事务该提交还是回滚,从而保证两份日志的一致,避免主库已提交但从库/恢复时出现不一致。它不提升写入性能,也不直接加速恢复或省内存,所以 A、C、D 不对。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "MVCC", + "隔离性", + "undo log", + "InnoDB" + ], + "question": "InnoDB 的 MVCC(多版本并发控制)实现中,普通 SELECT(非 locking read)读取的历史版本数据存储在哪个结构中?", + "options": { + "A": "redo log", + "B": "Buffer Pool 的一处独立缓冲", + "C": "undo log(历史版本链)", + "D": "binlog 的历史记录" + }, + "answer": "C", + "explanation": "InnoDB 通过 undo log 维护行的多版本链:每次更新会保留旧版本,当前读(快照读)根据事务的 ReadView 沿着 undo 版本链找到事务可见的历史版本,从而实现非锁定的一致性读,提升并发。redo log 是物理重做,binlog 是逻辑日志,Buffer Pool 存的是页而非版本链,均不负责多版本数据。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "崩溃恢复", + "redo log", + "undo log", + "InnoDB" + ], + "question": "InnoDB 崩溃恢复的三阶段顺序正确的是:", + "options": { + "A": "扫描 redo log → 用 undo 回滚未提交事务 → 重做已提交事务", + "B": "扫描 redo log → 重做(应用)已提交事务 → 用 undo 回滚未提交事务", + "C": "先备份数据 → 再扫描 redo → 最后刷脏页", + "D": "先用 undo 重做 → 再用 redo 回滚" + }, + "answer": "B", + "explanation": "正确的三阶段是:首先扫描 redo log 确定需要恢复的 LSN 范围;然后重放(重做)已提交事务的 redo,使崩溃前已提交但未刷盘的修改恢复到数据页;最后通过两阶段提交的 xid 检查,用 undo log 回滚那些未提交(未在 binlog 中完整记录)的事务。A 和 D 顺序反了。C 是无关操作。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "innodb_flush_log_at_trx_commit", + "持久性", + "性能", + "配置" + ], + "question": "参数 innodb_flush_log_at_trx_commit=2 时,日志刷盘行为是:", + "options": { + "A": "每次提交都调用 fsync 强制刷到磁盘", + "B": "每秒刷一次磁盘,崩溃可能丢失近 1 秒已提交事务", + "C": "每次提交时只把日志写入操作系统缓存(Page Cache),由 OS 决定何时落盘,崩溃可能丢失近 1 秒已提交事务", + "D": "不写日志,完全不做持久化" + }, + "answer": "C", + "explanation": "innodb_flush_log_at_trx_commit 有三个取值:0=每秒刷盘一次(可能丢接近 1 秒数据);1=每次事务提交都 fsync 刷盘(最强持久性,也是 ACID 默认保证,推荐);2=每次提交只写入 OS 缓存(write,不 fsync),操作系统崩溃时可能丢接近 1 秒已提交事务,但 MySQL 进程崩溃时不会丢。A 是取值 1,B 是取值 0。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "隔离性", + "临键锁", + "间隙锁", + "next-key lock", + "InnoDB" + ], + "question": "在 RR(可重复读)隔离级别下,InnoDB 对普通索引范围查询加的是哪种锁,用来防止幻读?", + "options": { + "A": "只加行锁(record lock)", + "B": "临键锁(Next-Key Lock,行锁 + 间隙锁),锁定记录及其前面的间隙", + "C": "表级意向锁", + "D": "不加任何锁(快照读自动无锁)" + }, + "answer": "B", + "explanation": "在可重复读(RR)隔离级别下,InnoDB 对索引范围/普通查找使用临键锁(next-key lock = 行锁 + 间隙锁),相当于锁定左开右闭的区间,阻止其他事务在扫描区间插入新行,从而防止幻读。纯行锁(A)无法防幻读;意向锁(C)是表级锁标记,不直接防幻读;快照读虽然一般不加锁,但此题针对范围查询加锁场景,答案为 B。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "一致性", + "约束", + "ACID", + "InnoDB" + ], + "question": "从 ACID 的角度看,InnoDB 的“一致性”主要是由哪部分保证的?", + "options": { + "A": "完全由数据库自动保证,与应用无关", + "B": "由约束(主键、外键、唯一、非空、CHECK 等)保证,并且依赖原子性、隔离性、持久性共同维持事务前后一致的完整状态", + "C": "只靠锁机制保证,与约束无关", + "D": "只靠 redo log 保证" + }, + "answer": "B", + "explanation": "一致性(C)需要一个事务从一致的状态出发最终到另一个一致状态,它由数据库的约束(主外键、唯一、非空、CHECK)以及底层对每个原子、隔离、持久性共同实现:原子性或隔离的破坏都会破坏一致性。所以一致性不是某个单一机制,而是约束 + A、I、D 三者的合力。C、D 只提单一机制不完整,A 过于绝对(业务也要保证业务规则一致)。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/database/mysql-transaction-isolation/code_reading.json b/topics/database/mysql-transaction-isolation/code_reading.json new file mode 100644 index 0000000..a0253f1 --- /dev/null +++ b/topics/database/mysql-transaction-isolation/code_reading.json @@ -0,0 +1,257 @@ +{ + "topic": "mysql-transaction-isolation", + "type": "code_reading", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "cr-001", + "type": "code_reading", + "difficulty": 2, + "tags": [ + "隔离级别", + "脏读", + "READ UNCOMMITTED" + ], + "question": "隔离级别为 READ UNCOMMITTED 时的脏读场景。假设 stocks 表初始只有一行:id=1, price=100。两个会话按下列顺序执行(期间会话 A 尚未 COMMIT),请回答会话 B 的普通 SELECT 读到什么。", + "code": "-- 会话 B\nSET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;\n-- 会话 A\nSET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;\nBEGIN;\nUPDATE stocks SET price=50 WHERE id=1; -- A 执行,未提交\nSELECT price FROM stocks WHERE id=1; -- B 的普通 SELECT(快照读)\nROLLBACK; -- A 回滚", + "language": "sql", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "short_answer", + "question": "会话 B 的普通 SELECT 读到的 price 是多少?为什么会是这个值?这属于哪种并发异常?", + "answer": "读到 50。因为 READ UNCOMMITTED 允许读取其他事务尚未提交的最新修改,因此 B 的普通 SELECT 会直接读到 A 刚 UPDATE 但还未 ROLLBACK 的值 50,这就是脏读。", + "keywords": [ + "READ UNCOMMITTED", + "脏读", + "未提交", + "50", + "ROLLBACK" + ], + "scoring_rubric": "答出读到 50(得 50%);说明这是脏读(得 25%);说明原因是未判断未提交(得 25%)。", + "explanation": "READ UNCOMMITTED 下普通 SELECT 不做可见性过滤,直接读到 A 已修改但未提交的值 50;A 随后 ROLLBACK,说明 B 读到的数据从未真正存在过,这正是脏读。" + }, + { + "index": 2, + "type": "single_choice", + "question": "关于脏读发生的隔离级别,下列说法正确的是?", + "answer": "A", + "options": { + "A": "只有读未提交级别会出现脏读", + "B": "READ COMMITTED 级别也会允许读到未提交数据", + "C": "脏读在本场景只有在 A 提交后才会发生", + "D": "该场景不会发生脏读,因为没有 commit" + }, + "explanation": "脏读只出现在 READ UNCOMMITTED 级别。READ COMMITTED 及以上的快照读不会读到未提交数据。脏读与是否 COMMIT 无关,反而是「未提交仍被读到」才叫脏读。", + "keywords": [ + "脏读", + "READ UNCOMMITTED" + ] + } + ], + "explanation": "本案例展示最低隔离级别的脏读:B 的快照读在 READ UNCOMMITTED 下没有可见性过滤,直接读了 A 未提交的最新值 50,而 A 随后回滚,B 读到的就是脏数据。" + }, + { + "id": "cr-002", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "隔离级别", + "不可重复读", + "READ COMMITTED" + ], + "question": "隔离级别为 READ COMMITTED,scores 表初始有 id=1, score=80。判断 T-B 在同一事务内两次 SELECT 的返回结果。", + "code": "-- 会话 B\nSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;\n-- 会话 A\nSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;\nBEGIN;\nUPDATE scores SET score=90 WHERE id=1;\nCOMMIT;\nSELECT score FROM scores WHERE id=1; -- B 第一次 SELECT\nSELECT score FROM scores WHERE id=1; -- B 第二次 SELECT", + "language": "sql", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "short_answer", + "question": "会话 B 在同一个事务内两次 SELECT 分别读到什么值?为什么会出现这种结果?", + "answer": "第一次读到 80(A 提交前的旧值),第二次读到 90(A 已提交后的新值)。因为 READ COMMITTED 每次普通 SELECT 都新建 ReadView,能看到最新已提交的版本,故同一事务内两次结果不同,即不可重复读。", + "keywords": [ + "READ COMMITTED", + "不可重复读", + "新 ReadView", + "80", + "90" + ], + "scoring_rubric": "正确写出第一次 80、第二次 90(各 3 分);指出原因在于每次 SELECT 新建 ReadView(2 分),说明这是不可重复读(2 分)。", + "explanation": "READ COMMITTED 每次普通 SELECT 都新建 Read View,因此第二次读能看到 A 已提交的新值 90,同一事务内两次读结果不同,即不可重复读。" + }, + { + "index": 2, + "type": "single_choice", + "question": "关于本场景产生不可重复读的根本原因,下列说法正确的是?", + "answer": "B", + "options": { + "A": "RC 级别不会产生不可重复读", + "B": "由于两次 SELECT 都新建 ReadView,因此结果不同", + "C": "由于 RR 复用 ReadView,所以本场景结果相同", + "D": "以上都不对" + }, + "explanation": "RC 下每次快照读新建 ReadView 是产生不可重复读的根本原因。本场景隔离级别是 RC 而非 RR,不能套用 RR 的 ReadView 复用逻辑。", + "keywords": [ + "不可重复读", + "ReadView" + ] + } + ], + "explanation": "经典不可重复读:READ COMMITTED 每次 SELECT 新建 ReadView,第一次读到 A 提交前的 80,第二次读到 A 提交后的 90,两次结果不一致。" + }, + { + "id": "cr-003", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "隔离级别", + "可重复读", + "REPEATABLE READ", + "Read View" + ], + "question": "隔离级别为 REPEATABLE READ(MySQL 默认),scores 表 id=1, score=80。B 在同一事务内先建立 ReadView,再多次 SELECT,判断每次能否看到 A 提交的新值。", + "code": "-- 会话 B\nSET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n-- 会话 A\nSET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;\nBEGIN;\nSELECT score FROM scores WHERE id=1; -- B 第一条 SELECT,建立 ReadView\nUPDATE scores SET score=50 WHERE id=1;\nCOMMIT;\nSELECT score FROM scores WHERE id=1; -- B 快照读,复用 ReadView\nSELECT score FROM scores WHERE id=1; -- B 快照读,仍复用同一 ReadView", + "language": "sql", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "short_answer", + "question": "会话 B 的两次快照读分别读到什么值?请结合 Read View 的创建时机说明原因。", + "answer": "两次快照读都读到 80,而不是 A 提交的 50。因为 RR 下事务 B 第一条 SELECT 构建的 ReadView 在整个事务内复用,A 的修改发生在该 ReadView 建立之后,A 在 B 的 ReadView 中属于不可见事务,因此 B 沿版本链始终读到旧值 80,实现了可重复读。", + "keywords": [ + "REPEATABLE READ", + "ReadView 复用", + "80", + "可重复读", + "快照读" + ], + "scoring_rubric": "答出两次都读 80(3 分);说明原因是 ReadView 复用且 A 不可见(3 分)。", + "explanation": "REPEATABLE READ 下事务 B 的第一条 SELECT 创建 Read View 并在整个事务内复用;A 的修改事务 ID 大于等于该视图的 max_trx_id(或在 m_ids 中),对 B 不可见,于是 B 沿版本链始终读到旧值 80。" + }, + { + "index": 2, + "type": "short_answer", + "question": "如果把隔离级别换成 READ COMMITTED,会话 B 第二次读的结果会变成什么?两种级别行为差异的本质是什么?", + "answer": "如果换成 READ COMMITTED,B 的第二次读就会新建 ReadView,看到 A 已提交的 50,产生不可重复读;而 RR 下复用第一个 ReadView,全程读到 80。两者行为差异的本质就在于 ReadView 的创建时机。", + "keywords": [ + "READ COMMITTED", + "每次新建", + "50", + "不可重复读" + ], + "scoring_rubric": "指出换成 RC 第二次读到 50(3 分);强调 RC 每次新建 / RR 复用 ReadView 的差异(3 分)。", + "explanation": "差异本质在 Read View 的创建时机:RC 每次快照读都新建,RR 只在事务首次快照读时创建并复用。" + } + ], + "explanation": "RC 与 RR 的对照:RR 复用首个 ReadView,快照读看到固定旧值 80,满足可重复读;换成 RC 每次新建 ReadView,则会看到 50。" + }, + { + "id": "cr-004", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "幻读", + "当前读", + "间隙锁", + "REPEATABLE READ" + ], + "question": "REPEATABLE READ 下,T-A 对区间执行当前读 SELECT ... FOR UPDATE,然后 T-B 尝试在该区间内插入一行,考察间隙锁/临键锁对插入的阻塞。表 orders(id, order_no) 已有数据 id=2、id=4(id 不连续)。", + "code": "-- 会话 A\nSET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;\nBEGIN;\n-- 表 orders(id, order_no) 已有数据 id=2 和 id=4\nSELECT * FROM orders WHERE id BETWEEN 1 AND 4 FOR UPDATE;\n-- 会话 B 尝试插入 id=3\nINSERT INTO orders(id, order_no) VALUES(3, 'phantom');", + "language": "sql", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "short_answer", + "question": "会话 B 的 INSERT 会立即成功还是被阻塞?请说明 A 加的是什么锁、锁住了哪个范围。", + "answer": "会话 B 的 INSERT 会因 T-A 持有的间隙锁/临键锁而被阻塞而无法即时成功。SELECT ... FOR UPDATE 是当前读,RR 下会对扫描区间内不存在记录的空隙施加 Gap Lock / Next-Key Lock,B 的 id=3 落在被锁的间隙 (2,4) 内,会被阻塞等待,直到 A 提交或回滚。", + "keywords": [ + "间隙锁", + "临键锁", + "阻塞", + "FOR UPDATE", + "幻读" + ], + "scoring_rubric": "答出插入被阻塞(3 分);说明原因是临键锁/间隙锁锁住 (2,4) 区间(3 分)。", + "explanation": "SELECT ... FOR UPDATE 是当前读,RR 下会对扫描区间加临键锁(记录锁 + 间隙锁),锁住 (2,4) 等间隙;B 要插入的 id=3 落在被锁间隙内,因此被阻塞直到 A 提交或回滚。" + }, + { + "index": 2, + "type": "single_choice", + "question": "关于会话 B 插入 id=3 的执行结果,下列说法正确的是?", + "answer": "B", + "options": { + "A": "B 插入立即成功,因为当前读只锁已存在的行", + "B": "B 插入被阻塞,因为当前读的临键锁锁住了包含 (2,4) 的间隙区间", + "C": "B 插入成功,因为 RR 不会发生幻读所以无需加锁", + "D": "SELECT ... FOR UPDATE 不加锁,不会影响插入" + }, + "explanation": "RR 下 FOR UPDATE 当前读会加临键锁(行锁 + 间隙锁),把区间间隙锁住。B 想插入 id=3,落在被锁的 (2,4) 区间内,因此被阻塞。这正是防幻读的锁机制。A、C、D 均错误。", + "keywords": [ + "临键锁", + "间隙", + "阻塞" + ] + } + ], + "explanation": "该例演示 RR 下非连续索引区间利用临键锁锁住间隙,阻断并发插入,实现当前读的幻读防护。" + }, + { + "id": "cr-005", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "隔离级别", + "脏读", + "快照读" + ], + "question": "隔离级别为 REPEATABLE READ,判断 B 能否读到 A 未提交的脏数据(与 READ UNCOMMITTED 对比)。", + "code": "-- 会话 B\nSET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n-- 会话 A\nSET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;\nBEGIN;\nUPDATE accounts SET balance=0 WHERE id=1; -- A 执行,未提交\nSELECT balance FROM accounts WHERE id=1; -- B 的普通 SELECT(快照读)", + "language": "sql", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "short_answer", + "question": "会话 B 的普通 SELECT 读到的 balance 是多少?请用 Read View 的可见性判断规则说明原因。", + "answer": "读到的是旧值(例如原 balance),不会读到 A 未提交的 0。因为 REPEATABLE READ 及以上级别不允许脏读,普通 SELECT 走快照读会用 ReadView 判断:A 未提交(在 m_ids 中)不可见,于是沿版本链读到已提交的旧版本。", + "keywords": [ + "不可读脏", + "未提交", + "ReadView", + "旧版本" + ], + "scoring_rubric": "答读不到未提交(3 分);说明 ReadView 判定 A 不可见(3 分)。", + "explanation": "A 的修改尚未提交,其事务 ID 在 B 的 Read View 的 m_ids 中,判定为不可见;B 沿 roll_ptr 找到上一个已提交版本,因此读到的是旧值而非 A 改的 0,不会发生脏读。" + }, + { + "index": 2, + "type": "single_choice", + "question": "关于 REPEATABLE READ 级别下普通 SELECT 的读取行为,下列说法正确的是?", + "answer": "B", + "options": { + "A": "普通 SELECT 一定读到最新未提交的值", + "B": "此级别下事务快照读只可能读到已提交(对本事务可见)的历史版本", + "C": "脏读在 RR 下也会发生", + "D": "普通 SELECT 会加排他锁以防读到脏数据" + }, + "explanation": "RR(及 RC)下普通 SELECT 是快照读,只读到通过 ReadView 判定可见的已提交版本,不会读到未提交数据,因此脏读在该级别不会发生。B 正确。", + "keywords": [ + "快照读", + "脏读" + ] + } + ], + "explanation": "对比 READ UNCOMMITTED 与 REPEATABLE READ,RR 及以上级别的普通读不读未提交数据,不发生脏读。" + } + ] +} \ No newline at end of file diff --git a/topics/database/mysql-transaction-isolation/fill_blank.json b/topics/database/mysql-transaction-isolation/fill_blank.json new file mode 100644 index 0000000..de3a89c --- /dev/null +++ b/topics/database/mysql-transaction-isolation/fill_blank.json @@ -0,0 +1,201 @@ +{ + "topic": "mysql-transaction-isolation", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 1, + "tags": [ + "隔离级别" + ], + "question": "MySQL InnoDB 支持的四级事务隔离级别从低到高分别是:____(读未提交)、____(读已提交)、____(可重复读)、____(串行化),其中 MySQL 默认的隔离级别是____。", + "answer": [ + "READ UNCOMMITTED", + "READ COMMITTED", + "REPEATABLE READ", + "SERIALIZABLE", + "REPEATABLE READ" + ], + "answer_rule": "ordered", + "explanation": "ISO 定义的四隔离级别:READ UNCOMMITTED(允许脏读)、READ COMMITTED(只读已提交,出现不可重复读)、REPEATABLE READ(可重复读 + 间隙锁防幻读)、SERIALIZABLE(所有读加锁串行)。MySQL InnoDB 默认级别是 REPEATABLE READ。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "脏读", + "不可重复读", + "幻读" + ], + "question": "事务并发读取产生的三种异常现象分别是:____(读到其他事务未提交的数据)、____(同一事务中同一行两次读取的值不同)、____(同一事务中满足同一条件的行数发生变化,由其他事务插入造成)。", + "answer": [ + "脏读", + "不可重复读", + "幻读" + ], + "answer_rule": "ordered", + "explanation": "脏读(Dirty Read)读到未提交数据;不可重复读(Non-Repeatable Read)同一行两次值不一致;幻读(Phantom Read)满足条件的行数量发生变化(靠插入)。幻读比不可重复读高级,需要防止插入。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "MVCC", + "版本链" + ], + "question": "InnoDB 中每行数据通常隐藏着两个关键列:____(最后修改该行的事务 ID)与 ____(指向 undo log 中该行旧版本的指针),多个旧版本通过该指针串成一条____。", + "answer": [ + "DB_TRX_ID", + "DB_ROLL_PTR", + "版本链" + ], + "answer_rule": "ordered", + "explanation": "DB_TRX_ID 记录最后修改该行的事务 ID,DB_ROLL_PTR 指向 undo log 中的旧版本记录,从而将同一逻辑行的所有历史版本串联成版本链。MVCC 靠 ReadView 沿版本链回溯找到对当前事务可见的版本。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "Read View", + "MVCC" + ], + "question": "Read View 主要由四个字段构成:____(快照创建时未提交的事务 ID 集合,通常简称 m_ids)、____(该集合里最小的事务 ID)、____(当前已分配的最大事务 ID 加一或最大、用于判断新启动的事务)、____(当前事务的 ID)。", + "answer": [ + "m_ids", + "min_trx_id", + "max_trx_id", + "creator_trx_id" + ], + "answer_rule": "ordered", + "explanation": "ReadView 的四个核心字段:m_ids(未提交事务集合)、min_trx_id(其中最小 trx_id)、max_trx_id(下一条将要分配的事务 id,或最大 trx_id)、creator_trx_id(创建该 ReadView 的事务自身 id)。可见性判断以这四者为依据。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "Read View", + "版本链" + ], + "question": "设某行版本的事务 ID 为 trx_id。Read View 判断可见性的顺序中:若 trx_id 等于当前创建 Read View 的事务 ID,则____;若 trx_id < min_trx_id,说明该版本事务已提交,____;若 trx_id 大于等于 max_trx_id,说明该事务在快照创建后才启动,____;若 trx_id 处于 m_ids 集合中,说明该事务还未提交,____。", + "answer": [ + "可见", + "可见", + "不可见", + "不可见" + ], + "answer_rule": "ordered", + "explanation": "可见性判断顺序:① creator 自己(本事务)修改的版本可见;② trx_id < min_trx_id:已提交,可见;③ trx_id >= max_trx_id:快照之后才启动的事务,不可见;④ trx_id 在 m_ids 中:还未提交,不可见;⑤ 否则可见。若版本链头不可见,则沿 DB_ROLL_PTR 继续向前回溯找可见版本。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "快照读", + "当前读" + ], + "question": "普通 SELECT(不加锁,基于 MVCC 读一致性快照)称为____;SELECT ... FOR UPDATE / UPDATE / DELETE / INSERT 读取最新已提交数据并加锁,称为____。", + "answer": [ + "快照读", + "当前读" + ], + "answer_rule": "ordered", + "explanation": "快照读(Snapshot Read,也称一致性非锁定读)走 MVCC,不加锁;当前读(Current Read)读到最新已提交数据并对读取行加锁。当前读在 REPEATABLE READ 下配合间隙锁/临键锁可防止幻读。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "隔离级别", + "Read View" + ], + "question": "在 ____ 级别下,每执行一次普通 SELECT 都会新建 Read View,因此同一事务两次读取同一行可能得到不同值(不可重复读);在 ____ 级别下,事务的第一条 SELECT 建立 Read View 并在整个事务内复用,从而保证可重复读。", + "answer": [ + "READ COMMITTED", + "REPEATABLE READ" + ], + "answer_rule": "ordered", + "explanation": "RC:每次快照读新建 ReadView,能看到其他事务最新提交,导致不可重复读。RR:首个 SELECT(或首次访问行数据)创建 ReadView 后复用,整个事务看到的是一致快照,故可重复读。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "行锁", + "间隙锁", + "临键锁" + ], + "question": "InnoDB 的三种主要锁类型分别是:只锁定某条具体记录的____、锁定索引区间但不锁定具体记录的____(为防其他事务在此区间插入)、两种组合而成的____(同时锁定记录与其之前的区间)。", + "answer": [ + "Record Lock(行锁)", + "Gap Lock(间隙锁)", + "Next-Key Lock(临键锁)" + ], + "answer_rule": "ordered", + "explanation": "Record Lock 锁定单条记录;Gap Lock 锁定索引间的空隙(防止插入造成幻行,本身不保护已存在的具体行);Next-Key Lock 是 Record Lock 与 Gap Lock 的组合(前开后闭区间),既锁记录又锁间隙,是防幻读的核心手段。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "隔离级别", + "幻读" + ], + "question": "REPEATABLE READ 下,通过两条路径防止幻读:普通 SELECT 走 MVCC____,读取一致快照;而 SELECT ... FOR UPDATE 等____读则会使用临键锁/间隙锁锁定范围,阻止并发事务在范围内____(插入数据)从而产生幻读。", + "answer": [ + "快照读", + "当前读", + "插入" + ], + "answer_rule": "ordered", + "explanation": "RR 下防幻读是『快照读一致性』与『当前读加间隙锁』的结合:快照读读到的都是固定的旧快照,不会看到别的新插入行;当前读则通过 Gap Lock / Next-Key Lock 阻塞范围内的并发插入,保证当前读也不会读到幻行。注意这两种机制分开来看,单纯 MVCC 快照读其实不能完全阻止其他事务对旧区间('对本事务而言)被修改而产生的幻行,因此间隙锁仍是必要的。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "隔离级别", + "MVCC" + ], + "question": "MySQL 事务隔离性本身是通过『锁 + ____』两种机制共同实现的:锁用于解决____(写写)冲突以及当前读场景下的幻读防止(间隙锁/临键锁),而____用于实现多版本并发控制,使普通读(快照读)与写操作可以互不阻塞并行执行。", + "answer": [ + "MVCC", + "写", + "MVCC" + ], + "answer_rule": "ordered", + "explanation": "锁解决写写互斥与区间防插入,MVCC(Multi-Version Concurrency Control)解决读写之间不互斥(快照读不加锁)。两者叠加才完整支撑 InnoDB 的隔离语义。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/database/mysql-transaction-isolation/meta.json b/topics/database/mysql-transaction-isolation/meta.json new file mode 100644 index 0000000..ef26f07 --- /dev/null +++ b/topics/database/mysql-transaction-isolation/meta.json @@ -0,0 +1,41 @@ +{ + "slug": "mysql-transaction-isolation", + "name": "MySQL 事务与隔离机制", + "description": "四种隔离级别、脏读/不可重复读/幻读、MVCC Read View 选版本逻辑、Repeative Read 与 Next-Key Lock", + "tags": [ + "MVCC", + "Read View", + "undo log", + "不可重复读", + "临键锁", + "幻读", + "当前读", + "快照读", + "版本链", + "脏读", + "行锁", + "间隙锁", + "隔离级别" + ], + "difficulty_range": [ + 1, + 4 + ], + "schema_version": "1.0.0", + "updated": "2026-09-07", + "question_files": [ + "single_choice", + "fill_blank", + "code_reading", + "short_answer" + ], + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "fill_blank": 10, + "code_reading": 5, + "short_answer": 10 + } + } +} diff --git a/topics/database/mysql-transaction-isolation/short_answer.json b/topics/database/mysql-transaction-isolation/short_answer.json new file mode 100644 index 0000000..3e917a7 --- /dev/null +++ b/topics/database/mysql-transaction-isolation/short_answer.json @@ -0,0 +1,213 @@ +{ + "topic": "mysql-transaction-isolation", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 1, + "tags": [ + "隔离级别" + ], + "question": "请列出 MySQL InnoDB 支持的四种事务隔离级别,并按隔离强度从低到高说明它们的名称(中英文均可)。", + "answer": "四种隔离级别从低到高依次是:READ UNCOMMITTED(读未提交)、READ COMMITTED(读已提交)、REPEATABLE READ(可重复读,MySQL 默认)、SERIALIZABLE(串行化)。", + "keywords": [ + "READ UNCOMMITTED", + "READ COMMITTED", + "REPEATABLE READ", + "SERIALIZABLE", + "默认" + ], + "scoring_rubric": "正确列出四个名称各 2 分(共 8 分);答对从低到高顺序并指出 REPEATABLE READ 是 MySQL 默认级别得 2 分。", + "explanation": "四个隔离级别的强度递增:RU 最低允许脏读;RC 只读已提交但出现不可重复读;RR 额外用 MVCC 快照与间隙锁/临键锁防止幻读(MySQL InnoDB 默认);SERIALIZABLE 最高,所有读都加锁串行。" + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "脏读", + "不可重复读", + "幻读" + ], + "question": "请分别解释事务并发过程中出现的三个异常现象:脏读、不可重复读、幻读。", + "answer": "脏读:一个事务读到了另一个事务尚未提交(可能回滚)的数据。不可重复读:同一事务内,两次读取同一行数据得到不同的值,因为期间有其他事务修改并提交了该行。幻读:同一事务内,两次执行同一条件查询得到的行数不同,因为另一事务在期间插入或删除了满足条件的行(幻读主要由插入产生)。", + "keywords": [ + "脏读", + "不可重复读", + "幻读", + "未提交", + "行数变化" + ], + "scoring_rubric": "三个现象各 3 分,正确描述现象并点出各自关键特征(脏读=未提交、不可重复读=同一行值变化、幻读=行数变化)得满分;部分答对按要点给分。", + "explanation": "三者隔离强度递进:脏读最严重(连提交状态都不判断);不可重复读是同一行值变化;幻读是结果集行数变化。SERIALIZABLE 能杜绝全部三种;RR 靠快照读 + 间隙锁基本防住幻读;RC 会发生不可重复读。" + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "隔离级别", + "脏读", + "不可重复读", + "幻读" + ], + "question": "请分别说明四种隔离级别各自允许哪些并发异常(脏读、不可重复读、幻读)。", + "answer": "READ UNCOMMITTED:三种异常都可能出现(脏读、不可重复读、幻读)。READ COMMITTED:不允许脏读,但可能出现不可重复读;对幻读在 MySQL 的语义下基本不设防。REPEATABLE READ:不允许脏读与不可重复读,通过 MVCC 快照读与间隙锁/临键锁基本防止了幻读。SERIALIZABLE:三种异常都不允许,所有读都加锁,事务串行执行。", + "keywords": [ + "脏读", + "不可重复读", + "幻读", + "各隔离级别", + "SERIALIZABLE" + ], + "scoring_rubric": "每个级别正确描述其允许/禁止的异常得 2.5 分(共 10 分),需同时说对允许或禁止哪些现象。", + "explanation": "隔离级别由低到高允许的异常:RU 全允许;RC 除脏读外其余可发生;RR 基本不允许脏读和不可重复读,幻读由 MVCC+锁基本消除;SERIALIZABLE 全禁止。" + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "MVCC", + "版本链", + "undo log" + ], + "question": "简述 MySQL InnoDB 的 MVCC(多版本并发控制)是什么?它依赖哪些隐藏列形成版本链?有何作用?", + "answer": "MVCC 即多版本并发控制,核心思路是让普通 SELECT(快照读)通过读取历史版本与写操作并行且不互斥。每行会隐藏两个列:DB_TRX_ID(最后修改该行的事务 ID)和 DB_ROLL_PTR(指向 undo log 中旧版本的指针),多个历史版本通过 DB_ROLL_PTR 串成一条版本链。快照读依据 Read View 沿版本链找到对当前事务可见的那个版本。作用是实现读写不互斥,提高并发度,并支撑 RC 的读已提交与 RR 的可重复读。", + "keywords": [ + "MVCC", + "DB_TRX_ID", + "DB_ROLL_PTR", + "版本链", + "undo log", + "读写不互斥" + ], + "scoring_rubric": "说出 MVCC 使读写不互斥(2 分)、两个隐藏列 DB_TRX_ID/DB_ROLL_PTR(各 2 分)、版本链与 undo log(2 分)、讲清作用(2 分)。", + "explanation": "MVCC 是不加锁并发读的重要机制。undo log 保存旧版本,DB_ROLL_PTR 指向它们,从而把同一行的所有历史版本串成一条版本链。" + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "Read View", + "版本链" + ], + "question": "请描述 Read View 的四个核心字段,并用它们说明 MVCC 判断某行版本可见性的规则。", + "answer": "Read View 四个字段:m_ids(创建快照这一刻尚未提交的事务 ID 集合)、min_trx_id(m_ids 中最小的事务 ID)、max_trx_id(下一条将要分配的事务 ID 或最大事务 ID)、creator_trx_id(当前创建 ReadView 的事务自身 ID)。可见性判断顺序:一是版本 trx_id 等于 creator_trx_id(自己改的)则可见;二是 trx_id 小于 min_trx_id(该事务已提交且在快照前)则可见;三是 trx_id 大于等于 max_trx_id(快照之后才启动的事务)则不可见;四是 trx_id 处于 m_ids 集合中(尚未提交)则不可见;五是否则可见。若当前版本不可见,则沿 DB_ROLL_PTR 向版本链更旧版本回溯继续判断。", + "keywords": [ + "m_ids", + "min_trx_id", + "max_trx_id", + "creator_trx_id", + "可见性" + ], + "scoring_rubric": "正确写出四个字段各 1 分(共 4 分);可见性规则每条(自己可见、已提交可见、未提交不可见、新事务不可见)约 1.5 分,共约 6 分。", + "explanation": "ReadView 是 MVCC 可见性判断的核心,关键在于顺序判断:trx_id 小于 min 可见、大于等于 max 不可见、在 m_ids 中不可见。掌握它才能理解 RC 与 RR 的差异。" + }, + { + "id": "sa-006", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "隔离级别", + "Read View" + ], + "question": "为什么 READ COMMITTED 会发生不可重复读,而 REPEATABLE READ 不会?请从 Read View 的创建时机角度解释。", + "answer": "因为两者创建 ReadView 的时机不同。READ COMMITTED 下每执行一次普通 SELECT(快照读)都会新建一个 ReadView,因此后一次的 SELECT 能看到其他事务最新提交的数据,导致同一事务内两次读到不同值,即产生不可重复读。REPEATABLE READ 下,事务的第一条 SELECT 才创建 ReadView,之后整个事务内所有快照读都复用这一份 ReadView,其他事务的提交对这份固定快照不可见,因此多次读取结果一致,即满足可重复读。", + "keywords": [ + "新 ReadView", + "复用", + "每次 SELECT", + "不可重复读" + ], + "scoring_rubric": "指出 RC 每次都新建 ReadView(3 分)、RR 复用首次的 ReadView(3 分)、并正确连接到不可重复读与可重复读现象(4 分)。", + "explanation": "这正是 RC 与 RR 在 MVCC 上最本质的区别:ReadView 的创建时机与复用策略决定了可见版本集合的刷新频率。" + }, + { + "id": "sa-007", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "快照读", + "当前读" + ], + "question": "请解释 MySQL 中快照读与当前读的区别,并各举一个例子。", + "answer": "快照读(Snapshot Read,也称一致性非锁定读):普通的 SELECT,不加任何锁,通过 MVCC 读取一致性快照(依据 ReadView),不会因其它事务的写操作而阻塞。例子:SELECT * FROM t(不带 FOR UPDATE 或 LOCK IN SHARE MODE)。当前读(Current Read):读取数据的最新已提交版本,并在读取后被访问的影响记录加锁,属于加锁读。例子:SELECT ... FOR UPDATE(排X锁)、SELECT ... LOCK IN SHARE MODE(共享锁),以及 UPDATE、DELETE、INSERT 语句本身就是当前读。核心区别:快照读走 MVCC 不加锁,当前读走锁、读最新提交数据。", + "keywords": [ + "快照读", + "当前读", + "MVCC", + "加锁", + "FOR UPDATE" + ], + "scoring_rubric": "将快照读解释为 MVCC 不加锁(3 分)、当前读解释为加锁读最新(3 分)、各举正确例子(4 分)。", + "explanation": "快照读与当前读是理解 MySQL 并发读与锁行为的关键。注意 UPDATE/DELETE/INSERT 必须读最新提交并加锁,避免基于旧数据覆盖。" + }, + { + "id": "sa-008", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "临键锁", + "间隙锁", + "行锁", + "幻读" + ], + "question": "解释 Record Lock、Gap Lock、Next-Key Lock 三者的区别,以及它们如何配合防止幻读。", + "answer": "Record Lock(行锁):只锁定某一条具体记录,阻止对该行的修改。Gap Lock(间隙锁):锁定某个索引区间内的空隙(不含已存在的记录),阻止其他事务在这个空隙内插入新行,但它本身不保护具体记录,且间隙锁之间通常不互相排斥。Next-Key Lock(临键锁):是 Record Lock 与 Gap Lock 的组合,防护左开右闭区间,同时锁定记录本身及其之前的间隙。防幻读机制:REPEATABLE READ 下的当前读(如 SELECT ... FOR UPDATE、UPDATE、DELETE)会加 Next-Key 锁,把范围内所有可能插入的位置锁住,从而阻止并发事务插入新行,避免出现幻行。", + "keywords": [ + "Record Lock", + "Gap Lock", + "Next-Key Lock", + "防幻读", + "间隙" + ], + "scoring_rubric": "分别描述三种锁的特征各 2 分(共 6 分);解释临键锁如何通过间隙锁阻止插入防幻方 4 分。", + "explanation": "Next-Key Lock 等于 Record Lock 加 Gap Lock,是 RR 下当前读防幻读的关键。间隙锁专为堵插入而设计。" + }, + { + "id": "sa-009", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "隔离级别" + ], + "question": "如果业务需要最严格的隔离保证(不允许脏读、不可重复读、幻读),你会选择哪个隔离级别?代价是什么?", + "answer": "选择 SERIALIZABLE(串行化)。它是最高隔离级别,会强制将所有普通 SELECT 也转为加锁读(等价于给读取范围加共享锁并配合临键锁/间隙锁),读写完全互斥、事务近乎串行,从而杜绝脏读、不可重复读和幻读。代价是并发度大幅下降,读操作之间也互斥,容易引发锁竞争、阻塞甚至死锁,吞吐量明显低于其他级别;因此若非必要一般不直接使用,可在业务层用更精细的锁或优化 SQL 替代。", + "keywords": [ + "SERIALIZABLE", + "串行化", + "加锁读", + "并发下降", + "代价" + ], + "scoring_rubric": "答对选择 SERIALIZABLE(3 分);说明其加锁机制保证全隔离(3 分);指出以并发与性能为代价(4 分)。", + "explanation": "SERIALIZABLE 是安全但昂贵的:全部读都加锁,牺牲了 MVCC 的读写并发优势,只适合极苛刻场景。" + }, + { + "id": "sa-010", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "MVCC", + "隔离级别", + "快照读" + ], + "question": "InnoDB 的 MVCC 为什么在 READ COMMITTED 与 REPEATABLE READ 下都能工作?两者基于 MVCC 提供的可读行为有何区别?", + "answer": "MVCC 是 InnoDB 中通过每行版本链(DB_TRX_ID + DB_ROLL_PTR)与 Read View 判断可见性的通用机制,因此在 RC 与 RR 两个隔离级别下都会启用 MVCC。行为区别:RC 每次普通 SELECT 新建 ReadView,能读其他事务最新已提交的版本,产生不可重复读;RR 在事务第一条 SELECT 建立 ReadView 并全程复用,因此整个事务内快照一致,满足可重复读。两者 MVCC 基础设施相同,但 ReadView 的创建/复用规则不同,导致对外行为不同。", + "keywords": [ + "MVCC", + "ReadView", + "RC 每次", + "RR 复用", + "可重复读" + ], + "scoring_rubric": "说明 MVCC 在两个级别均启用(3 分);RC 每次新建 ReadView 的行为(3 分);RR 每次复用 ReadView 的行为(4 分)。", + "explanation": "MVCC 是基础,真正区分 RC/RR 语义的是 ReadView 的创建时机与复用策略,理解这点就掌握了 MySQL 并发读的核心。" + } + ] +} \ No newline at end of file diff --git a/topics/database/mysql-transaction-isolation/single_choice.json b/topics/database/mysql-transaction-isolation/single_choice.json new file mode 100644 index 0000000..5ca6fdf --- /dev/null +++ b/topics/database/mysql-transaction-isolation/single_choice.json @@ -0,0 +1,211 @@ +{ + "topic": "mysql-transaction-isolation", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 1, + "tags": [ + "隔离级别", + "脏读" + ], + "question": "MySQL InnoDB 默认的事务隔离级别是下列哪一项?", + "options": { + "A": "READ UNCOMMITTED", + "B": "READ COMMITTED", + "C": "REPEATABLE READ", + "D": "SERIALIZABLE" + }, + "answer": "C", + "explanation": "MySQL InnoDB 存储引擎的默认隔离级别是 REPEATABLE READ(可重复读)。在该级别下,同一事务内多次普通 SELECT 读取到的数据是一致的(快照读复用 Read View),并通过 MVCC + 间隙锁/临键锁在一定程度上防止了幻读。注意:虽然 InnDB 默认是可重复读,但 MySQL 官方文档同时指出 InnoDB 在该级别下结合唯一索引的临键锁几乎已能达到可串行化效果。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 1, + "tags": [ + "脏读" + ], + "question": "下列关于「脏读」的描述,正确的是?", + "options": { + "A": "事务 A 读到事务 B 已提交的新数据", + "B": "事务 A 读到事务 B 尚未提交(可能回滚)的数据", + "C": "同一事务内两次读同一行的值发生变化", + "D": "同一事务内满足条件的行数发生变化" + }, + "answer": "B", + "explanation": "脏读(Dirty Read)指一个事务读取到了另一个事务尚未提交的数据。由于该事务可能回滚,读到的值很可能是『脏』的、最终不存在的数据,因此称为脏读。选项 A 描述的是已提交数据,不算脏读;选项 C 是不可重复读;选项 D 是幻读。脏读只在最低的 READ UNCOMMITTED 隔离级别下发生。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "隔离级别", + "不可重复读" + ], + "question": "在 READ COMMITTED(读已提交)隔离级别下,下列哪种现象仍然可能发生?", + "options": { + "A": "脏读", + "B": "不可重复读", + "C": "幻读", + "D": "以上都不会发生" + }, + "answer": "B", + "explanation": "READ COMMITTED 通过只读已提交的数据消除了脏读,但每次普通 SELECT 都会新建 ReadView,因此同一事务内两次读取同一行,若期间有其他事务提交了更新,读取的值会不同,即产生不可重复读。幻读在某些定义下在 RC 级别并不存在(RC 只会插入符合条件的新行且事务无法保证前者被读出来的行数),但按照 MySQL 隔离级别标准,RC 允许不可重复读;选项 C 幻读严格发生在 REPEATABLE READ 以下,RC 级别在并发插入时也会出现类似幻读现象,但本题考察的核心隔离级别行为差异,正确答案选 B。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "幻读", + "间隙锁", + "临键锁" + ], + "question": "REPEATABLE READ(可重复读)隔离级别下,InnoDB 主要依靠什么机制来防止幻读?", + "options": { + "A": "完全是普通的行锁 Record Lock", + "B": "MVCC 快照读 + 间隙锁(Gap Lock)/ 临键锁(Next-Key Lock)", + "C": "每次查询都加全表锁", + "D": "完全依靠组合唯一约束" + }, + "answer": "B", + "explanation": "幻觉读(Phantom Read)是指同一事务内,满足相同条件的行数发生了增减(通过其他事务的插入实现)。InnoDB 在 REPEATABLE READ 下通过两条路径防止幻读:(1) 普通 SELECT 走快照读的 MVCC,读到的是一次快照;(2) 当前读(SELECT ... FOR UPDATE / UPDATE / DELETE)使用间隙锁 Gap Lock 与临键锁 Next-Key Lock(行锁 + 间隙锁)锁定区间,阻止并发事务在区间内插入新行。选项 A 忽略了间隙锁部分,选项 C 不准确,D 与本题无关。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "隔离级别", + "脏读" + ], + "question": "下列哪个隔离级别允许脏读发生?", + "options": { + "A": "READ UNCOMMITTED", + "B": "READ COMMITTED", + "C": "REPEATABLE READ", + "D": "SERIALIZABLE" + }, + "answer": "A", + "explanation": "READ UNCOMMITTED(读未提交)是对隔离要求最低的级别,它允许事务读取到其他事务尚未提交的数据,即允许脏读。该级别虽然并发性能较高,但语义上几乎难以保证正确性,因此上生产环境通常不用它。BCD 三个级别都不允许脏读。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "MVCC", + "Read View", + "版本链" + ], + "question": "下列关于 MySQL InnoDB MVCC 的说法,正确的是?", + "options": { + "A": "MVCC 使得读写操作完全相互排斥,性能下降", + "B": "每一行数据通常隐藏 DB_TRX_ID(最后修改事务 ID)与 DB_ROLL_PTR(指向 undo log 旧版本)两个隐藏列,共同构成版本链", + "C": "MVCC 只在 SERIALIZABLE 隔离级别下有效", + "D": "每次 UPDATE 都会覆盖旧版本数据,不保留历史版本" + }, + "answer": "B", + "explanation": "MVCC(多版本并发控制)的核心思想是让普通读(快照读)与写操作不用互相阻塞,从而显著提高并发性能,故 A 错误。InnoDB 会为每行隐藏 DB_TRX_ID(最后修改该行的事务 ID)与 DB_ROLL_PTR(指向 undo log 中旧版本的指针),由此形成一条版本链,快照读通过与 ReadView 比对找到可见的版本,故 B 正确。MVCC 在 READ COMMITTED 与 REPEATABLE READ 级别下都发挥作用,并非只在 SERIALIZABLE 下有效,故 C 错误。UPDATE 实际上会给行生成新版本并通过 DB_ROLL_PTR 指向旧版本,并不会无条件覆盖历史版本,故 D 错误。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Read View", + "快照读" + ], + "question": "关于 MySQL 中快照读(Snapshot Read)与当前读(Current Read)的说法,正确的是?", + "options": { + "A": "普通 SELECT 属于当前读,会加锁", + "B": "SELECT ... FROM t FOR UPDATE 属于快照读,走 MVCC 不加锁", + "C": "普通 SELECT 属于快照读,走 MVCC 不加锁;SELECT ... FOR UPDATE / UPDATE / DELETE / INSERT 属于当前读,走锁读最新已提交数据", + "D": "快照读和当前读都会对读取的行加排他锁" + }, + "answer": "C", + "explanation": "快照读(快照读)即普通 SELECT,它借助 MVCC 读取某个一致性快照,不加锁,因此称为非锁定读;而当前读是指需要读取数据最新已提交版本并在读取后对该行加锁的读取方式,包括 SELECT ... FOR UPDATE(加排他锁)、SELECT ... LOCK IN SHARE MODE(加共享锁)、以及 UPDATE / DELETE / INSERT。快照读在 REPEATABLE READ 下复用同一 ReadView,实现可重复读;当前读无论隔离级别如何都会读到最新已提交(或本事务)的数据,并可能触发间隙锁等。故 C 正确。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Read View", + "版本链", + "MVCC" + ], + "question": "假设 Read View 的快照包含:m_ids = {100, 101},min_trx_id = 100,max_trx_id = 102,creator_trx_id = 99。对于一个事务 ID 为 100 的事务修改过的数据行,该快照读时该行的可见性判断是?", + "options": { + "A": "可见,因为 100 小于 max_trx_id", + "B": "不可见,因为 100 在 m_ids 集合中(尚未提交)", + "C": "可见,因为在 m_ids 中属于已提交事务", + "D": "不可见,因为没有该行数据" + }, + "answer": "B", + "explanation": "MVCC Read View 可见性判断规则依次为:① 当前事务自己修改的(叶子 trx_id == creator_trx_id)可见;② trx_id < min_trx_id 说明已早期提交,可见;③ trx_id >= min_trx_id 不可见;④ trx_id 在 m_ids 集合中说明该事务尚未提交,不可见;⑤ 否则可见。本题中 trx_id = 100 恰好等于 min_trx_id 且在 m_ids 集合中,属于未提交事务,因此该行对当前 Read View 不可见(但是对自身事务可见)。B 正确。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "隔离级别", + "Read View" + ], + "question": "下列关于 READ COMMITTED 与 REPEATABLE READ 在 ReadView 上的区别,陈述正确的是?", + "options": { + "A": "RC 每次普通查询都新建 ReadView,RR 在事务第一条 SELECT 时建立 ReadView 并复用", + "B": "RC 在事务开始时建立 ReadView,RR 每次查询都新建", + "C": "二者都复用首个 ReadView", + "D": "二者每次查询都新建 ReadView" + }, + "answer": "A", + "explanation": "ReadView 的创建时机是 RC 与 RR 的关键差异:READ COMMITTED 下每执行一次普通 SELECT(快照读)都会新建一个 ReadView,因此可以读到其他事务最新提交的数据,产生不可重复读;REPEATABLE READ 下,事务第一条 SELECT(或数据变更触发时)构建 ReadView,之后整个事务中所有快照读都复用这份 ReadView,从而实现对同一条快照的一直读,满足可重复读。这也是 InnoDB 中 RR 通过快照实现可重复读的基础。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "隔离级别", + "脏读", + "临键锁" + ], + "question": "SERIALIZABLE(串行化)隔离级别与 MySQL InnoDB 实现中最相符的描述是?", + "options": { + "A": "仅对写操作加锁,读完全不加锁", + "B": "使所有普通 SELECT 也被强制转为加锁读(等价当前读),并对范围加临键锁,实时上所有读写操作都是互斥串行执行", + "C": "该级别也依赖 MVCC 快照读实现幻读防止", + "D": "只阻止脏读,不阻止幻读,所有读写通用 CS 频繁冲突" + }, + "answer": "B", + "explanation": "SERIALIZABLE 是最严格的隔离级别,它本质上将所有普通 SELECT 也自动升级为加锁读(等价于 SELECT ... LOCK IN SHARE MODE 的行为),读写完全相互阻塞,并配合间隙锁/临键锁串行化整个事务执行,因此既不会出现脏读、不可重复读,也不会出现幻读。选项 C 表面上在说 MVCC,但该级别下快照读会被锁读替代,不再单纯依赖 MVCC 快照,因此不准确;选项 D 错误。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/database/redis-persistence/code_reading.json b/topics/database/redis-persistence/code_reading.json new file mode 100644 index 0000000..1ebd88c --- /dev/null +++ b/topics/database/redis-persistence/code_reading.json @@ -0,0 +1,254 @@ +{ + "topic": "redis-persistence", + "type": "code_reading", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "cr-001", + "type": "code_reading", + "difficulty": 2, + "tags": [ + "AOF", + "AOF重写", + "命令序列", + "状态快照" + ], + "question": "请阅读下面这段 AOF 文件里按时间顺序追加的命令流。假设这些命令都已被 Redis 执行并写入 AOF,随后触发了一次 AOF 重写(BGREWRITEAOF)。", + "code": "SET user:1001 zhangsan\nSET user:1001 zhangsanfeng\nSET user:1002 lisi\nDEL user:1001\nINCR view:homepage\nSET counter 5\nINCRBY counter 1\nDEL counter", + "language": "plaintext", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "经过 AOF 重写后,新 AOF 文件里针对 user:1001 这条 key 记录的命令是什么?", + "options": { + "A": "SET user:1001 zhangsan 和 SET user:1001 zhangdanfeng(保留两条)", + "B": "DEL user:1001(因为最后被删除了)", + "C": "SET user:1001 zhangdanfeng(最终值)", + "D": "不写该 key(因为最终不存在)" + }, + "answer": "D", + "explanation": "AOF 重写不再按命令逐条重放,而是遍历当前内存数据库的状态,为每个 key 生成一条能重建其最终值的命令。user:1001 的最后一条命令是 DEL,即该 key 当前已不存在,因此重写后根本不生成任何关于 user:1001 的命令。A 保留了被覆盖的旧命令,B 生成一条 DEL 是错误的(key 已无,无需再删),C 误以为留下中间值,都错。" + }, + { + "index": 2, + "type": "short_answer", + "question": "重写后,counter 和 view:home 这两个 key 对应的命令分别是什么?为什么 AOF 重写能把这些命令从 8 条大幅精简?", + "answer": "重写后:counter 被最后一条 DEL 删除,key 已不存在,所以不生成命令;view:home 只剩 INCRBY view:home 1。\n\n能大幅精简的根本原因:AOF 存的是『命令序列』(append-only log),大量早期命令的中间结果会被后续命令覆盖(user:1001 被反复覆盖后删除、counter 被 INC/INCRBY 后再 DEL)。AOF 重写遍历当前内存哈希表,为每个 key 只生成一条『重建当前最终状态』的最小命令,把存储的具体『操作时间线』等价转换为『键值状态快照』,冗余的历史操作被丢弃,文件因此变小,恢复也更快。", + "explanation": "本题考察 AOF 重写的本质:将『命令序列』重新组织为『状态快照』。counter 终态不存在故无命令;只有仍存在的 key 才生成最小重建命令。压缩的核心依据是:大量命令对同一 key 是互相覆盖的,只保留最终值就能恢复一致状态。", + "keywords": [ + "不存在", + "状态快照", + "命令序列", + "重建最终值", + "最小命令", + "覆盖", + "压缩" + ], + "scoring_rubric": "①答出 counter 不存在、view 为 INCRBY 得 1 分;②答出 counter key 已删不生成命令得 1 分;③说明重写是把命令序列转换为键值状态快照、丢弃被覆盖的中间命令、文件变小得 3 分。" + } + ], + "explanation": "AOF 本身是一条一条 append 命令的时间线(log 结构),AOF 重写则遍历当前内存把时间线精简为哈希的 key→最终状态。凡是被删除或被后续命令覆盖的中间命令全部丢弃,达到『压缩』。这是理解 AOF 能压缩的本质(log 结构→状态快照)。" + }, + { + "id": "cr-002", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "AOF", + "RDB", + "恢复", + "持久化配置" + ], + "question": "下面是一台 Redis 实例的持久化配置(精简展示)。请阅读配置并回答进程崩溃重启时的数据恢复行为。", + "code": "# redis.conf 片段\nsave 900 1\nsave 300 10\nappendonly no", + "language": "bash", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "以上配置下,若 Redis 距离上一次 RDB 快照 10 分钟时进程宕机,最多可能丢失多少数据?", + "options": { + "A": "0,因为 Redis 每次写都实时落盘", + "B": "最近 5 分钟内写入的所有数据(有 save 300 的兜底)", + "C": "从上一次成功完成 RDB 快照之后到宕机前写操作的所有数据", + "D": "最后 30 秒的数据" + }, + "answer": "C", + "explanation": "本配置中 appendonly 为 no(关闭),只靠 RDB 快照持久化。RDB 是『定时的全量快照』,save 300 1 表示 300 秒内至少有 1 次写才触发一次生成点。宕机时只恢复到最近一次成功快照,快照之后的所有写操作全部丢失。所以『最长可能丢失上一次快照到宕机之间的全部写数据』(选 C)。A 错误,Redis 不是每次写都落盘;B 混淆了 save 触发间隔与丢数据范围;D 是 AOF 场景不适用于这里。" + }, + { + "index": 2, + "type": "short_answer", + "question": "同样的业务若想几乎不丢数据,应如何调整配置?为什么?", + "answer": "开启 AOF(将 appendonly 改为 yes),并根据数据重要程度选择同步策略,例如 appendfsync always(每条写命令都 fsync,最安全但性能低,几乎不丢)或 everysec(每秒同步,最多丢约 1 秒数据)。原因:RDB 是周期快照,快照间隔之间的数据没有任何日志兜底;AOF 是追加写命令日志,崩溃后通过重放命令恢复,丢失窗口被压缩到 appendfsync 决定的粒度(always=几乎不丢,everysec≤1s,如果需要可用 appendfsync no 让系统决定)。", + "explanation": "本题考察 RDB vs AOF 在数据安全上的取舍。开启 AOF 后,丢失窗口会从『上一次快照点』缩小到『appendfsync 策略决定的粒度』。平常推荐 everysec(性能/安全平衡),绝对不可丢的选 always。", + "keywords": [ + "appendonly yes", + "appendfsync", + "everysec", + "always", + "重放", + "丢失窗口" + ], + "scoring_rubric": "①答出开启 appendonly/AOF 得 1 分;②答出 appendfsync 取值及对应丢失窗口得 2 分;③说明 AOF 基于命令重放还原、丢失窗口从 RDB 快照周期缩小到同步粒度得 2 分。" + } + ], + "explanation": "RDB 快照与 AOF 日志在『丢数据范围』上差异极大:RDB 丢到上次快照,AOF 丢到 appendfsync 粒度。本题考查在给定持久化配置下计算最大丢数据量、以及如何调整降低丢失。" + }, + { + "id": "cr-003", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "redo log", + "环形缓冲", + "崩溃恢复", + "循环复用" + ], + "question": "下面用伪代码描述 InnoDB redo log 的『环形』复用思想。阅读并回答:为什么 redo log 不像 AOF 那样无限膨胀、也不像 AOF 需要主动重写压缩。", + "code": "# 伪代码:redo 环形缓冲区覆盖逻辑\ndef write_redo(head_lsn, tail_lsn, capacity):\n # 环形缓冲区大小固定(如 1GB)\n while (head_lsn - tail_lsn) >= capacity:\n flush_dirty_page_to_disk() # 强制刷脏页,推进 rev per tail/checkpoint\n append_record(page_lsn)\n head_lsn = head_lsn + 1", + "language": "readtext", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "redo log 采用『环形固定大小』覆盖最旧记录,覆盖旧记录最典型的前提条件是什么?", + "options": { + "A": "只要文件满了就直接覆盖最旧记录,不考虑数据页是否已落盘", + "B": "被覆盖的记录所对应的脏页必须先成功刷回磁盘(checkpoint 推进),否则崩溃会丢数据", + "C": "先把 redo log 文件长度翻倍再覆盖", + "D": "每次启动都清空整个 redo 文件" + }, + "answer": "B", + "explanation": "redo log 用于崩溃后重放:它记录的是尚未安全刷盘的数据页修改。覆盖旧记录的前提是,这些旧记录对应的脏页已经成功刷到数据文件(即 checkpoint 位置推进),否则一旦被覆盖而对应页面还没落盘,崩溃后就无法重放、造成永久丢失。所以先触发脏页刷盘(推进 checkpoint)再覆盖。A 直接覆盖会丢数据,C/D 与场景无关。" + }, + { + "index": 2, + "type": "short_answer", + "question": "从『数据结构(覆盖 vs 追加)』角度,说明 redo log 为何天然不膨胀、不需要像 AOF 那样主动重写;它们对中间过程的保留策略分别是什么?", + "answer": "redo log 是固定大小的环形缓冲(记录按 LSN 排序),当旧记录对应的脏页已落盘后,新记录就覆盖旧记录,天然丢弃了『已安全落盘、无需再重放』的物理页变更,只保留尚未落盘的部分,文件体积固定、不膨胀,无需主动重写。AOF 是末尾追加、累进的命令序列,命令一 append 就永久保留,会随时间无限膨胀,才需要 BGREWRITEAOF 主动把命令序列压缩成状态快照。\n对中间过程:redo 是物理层覆盖合并,不保留每次中间变更,重放只针对尚未落盘的最终页状态;AOF 则保留全部命令历史,直到重写时才丢弃被覆盖/删除的命令。", + "explanation": "二者数据结构(环形覆盖 vs 末尾追加)决定了是否需要主动压缩。redo『环形+checkpoint 推进』→ 天然不膨胀;AOF『append 追加』→ 需显式重写。这正是题目伪代码(后半是覆盖)与 AOF(后半是追加)的对照。", + "keywords": [ + "环形缓冲", + "覆盖", + "checkpoint", + "脏页落盘", + "LSN", + "不膨胀", + "追加", + "重写", + "状态快照" + ], + "scoring_rubric": "①答出 redo 是环形固定大小、覆盖已落盘旧页、天然不膨胀得 2 分;②答出 AOF 是末尾追加、积累全部命令、才需显式重写得 2 分;③说到中间过程差异(物理覆盖合并 vs 保留历史到重写)得 1 分。" + } + ], + "explanation": "关键在『覆盖 vs 追加』的数据结构差异:redo 环形覆盖天然合并中间过程、体积固定,AOF 追加增长→需要主动重写。这正是 AOF 为何要压缩而 redo 不用压缩的根因。" + }, + { + "id": "cr-004", + "type": "code_reading", + "difficulty": 2, + "tags": [ + "binlog", + "PITR", + "时间点恢复", + "mysqlbinlog" + ], + "question": "某运维在 14:00 误删除了一张表,希望恢复到 13:59 的状态。已有 12:00 的全量备份。阅读下面的恢复思路并回答。", + "code": "# 1. 先恢复 12:00 的全量备份\ntar -xzf backup_1200.tar.gz && mysql < backup_1200.sql\n\n# 2. 再用 binlog 做增量重放,停在误操作前\nmysqlbinlog --start-position= --stop-position= binlog.000123 | mysql", + "language": "bash", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "binlog 做时间点恢复时,'' 为什么用 '--stop-position'(文件+偏移)比单纯用 '--stop-datetime'(时间字符串)更能保证精确?", + "options": { + "A": "--stop-datetime 只对 ROW 格式生效,对 statement 格式失效", + "B": "时间戳是按 SQL 语句粗略过滤的,可能在一条事务/语句内部截止,导致该事务只执行了一半;而 position 是 binlog 里精确的字节偏移,可对准完整事件边界", + "C": "--stop-position 参数并不存在,MySQL 只支持 --stop-datetime", + "D": "--stop-position 会把日志里的所有事务一起截断,无法只保留部分" + }, + "answer": "B", + "explanation": "binlog 中能唯一定位重放位置的是文件的物理偏移(position),而非时间。--stop-datetime 按语句/事务的记录时间过滤,可能把一个未结束的事务切在中间(一半重放一半不重放),破坏事务原子性;position 能精确对准一个事件边界,所以做 PITR 用它最安全。A、D 说法无依据,C 错误(--stop-position 存在)。" + }, + { + "index": 2, + "type": "short_answer", + "question": "『从全量备份 + binlog 增量重放』这一整套恢复操作官方叫什么?为什么 AOF 做不到像 binlog 这样精准重放到任意历史点?", + "answer": "这套操作叫 point-in-time recovery(PITR,时间点恢复)。binlog 为 PITR 设计:逐条记录事务(含文件号+字节位置),配合全量备份可精确拼到任何一个历史提交点;mysqlbinlog 支持 --start-position/--stop-position(字节精确)和 --stop-datetime(时间近似)。AOF 是 Redis 的持久化日志,虽然记录了全部写命令,但它是『从文件头按顺序重放到文件尾』,命令本身没有内建可精确到事务边界的 position 切片接口,也没有标准的『从中间某条开始/停止』重放工具,要从历史某一时刻恢复需要手工截断文件再重放,非常不便,因此只说『可大致回到历史某个时间点、但需手动截断』,不像 binlog 原生支持精确切片。", + "explanation": "PITR 是 binlog 的核心能力,靠每条记录的位置+完整事务边界。AOF 虽也是命令日志,但其定位是『崩溃后从头把内存状态重放到当前』,而非『精确停在历史某一事务横切』,加上缺少原生 position/时间点切片命令,所以只能手动截断。", + "keywords": [ + "PITR", + "时间点恢复", + "position", + "全量备份", + "增量重放", + "从头顺序重放", + "无时间切片" + ], + "scoring_rubric": "①答出 PITR 得 1 分;②说明 binlog 用备份+position 增量重放实现精确到点得 2 分;③点出 AOF 只能从头顺序重放到末尾、缺少原生时间点重放而需要手动截断得 2 分。" + } + ], + "explanation": "考察 binlog 与 AOF 在时间点恢复上的差异。核心:binlog 支持 position/位点(PITR 精确),AOF 只能从头到末尾顺序重放、无原生时间点切片。" + }, + { + "id": "cr-005", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "AOF", + "appendfsync", + "丢失窗口", + "AOF重写", + "配置" + ], + "question": "下图为某 Redis 的 AOF 相关配置片段。请理解其含义并回答相应问题。", + "code": "appendonly yes\nappendfsync everysec\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb", + "language": "bash", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "在 appendfsync everysec 下,当进程宕机(不是主机断电)时,最多大约丢失多少数据?", + "options": { + "A": "1 秒的写入数据", + "B": "0 数据,一条都不丢", + "C": "距离上一次自动重写之间的全部写入数据", + "D": "最多 64MB 数据" + }, + "answer": "A", + "explanation": "everysec 表示每 1 秒把 AOF 缓冲 fsync 到磁盘一次,因此最多丢失『最后一次 fsync 到崩溃之间』这一秒的写入。'always' 才是一条不丢(B 错);C 混淆了 AOF 重写间隔与丢失窗口;D 把 auto-aof-rewrite-min-size 当成丢数据量,错误。" + }, + { + "index": 2, + "type": "short_answer", + "question": "auto-aof-rewrite-percentage 100 和 auto-aof-rewrite-min-size 64mb 这两个参数控制什么行为?这里的『重写』做了什么?", + "answer": "它们控制 AOF 自动重写(BGREWRITEAOF)的触发条件:当当前 AOF 文件体积相对上次重写后的文件增长幅度超过 100%(即翻倍),并且当前文件大小已经超过 64MB 的下限时,Redis 自动在后台触发一次 AOF 重写。\n重写的行为:遍历当前内存数据库,为每个 key 只重新生成一条重建其最终状态的最小命令(可配合前缀批量,尽量小段),丢弃掉大量被覆盖或已删除的旧命令,把膨胀的命令日志压缩为接近『状态快照』的小体积日志,文件变小,重启重放更快。", + "explanation": "auto-aof-rewrite-percentage/min-size 是 AOF 自动重写的双阈值:相对上一轮的增长百分比 + 最小文件下限,两者同时满足才自动触发。重写本质是把『命令日志 → 状态快照』压缩,避免 AOF 无限膨胀。", + "keywords": [ + "auto-aof-rewrite-percentage", + "100%增长", + "64MB下限", + "BGREWRITEAOF", + "状态快照", + "压缩" + ], + "scoring_rubric": "①解释 percentage 是相对上一轮增长幅度、min-size 是最小下限、需要同时满足得 2 分;②说明重写把命令日志转成最小状态快照、避免膨胀得 3 分。" + } + ], + "explanation": "考查 AOF 配置阈值与后台重写。appendfsync 决定丢失窗口,rewrite 阈值决定何时压缩 AOF 文件,两者结合掌握 AOF 的核心运维。" + } + ] +} \ No newline at end of file diff --git a/topics/database/redis-persistence/fill_blank.json b/topics/database/redis-persistence/fill_blank.json new file mode 100644 index 0000000..b12160a --- /dev/null +++ b/topics/database/redis-persistence/fill_blank.json @@ -0,0 +1,215 @@ +{ + "topic": "redis-persistence", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 1, + "tags": [ + "Redis", + "持久化", + "AOF", + "RDB" + ], + "question": "Redis 的 AOF 通过把每条____命令依次____到文件末尾,重启后按序____这些命令来恢复数据;Redis 的 RDB 则是把某时刻的____序列化成语义 sized 的全量快照文件。", + "answer": [ + "写", + "追加", + "重放", + "键值状态" + ], + "answer_rule": "ordered", + "explanation": "AOF 保存的是“命令”(逻辑增量),以追加方式写入,恢复时重放命令;RDB 保存的是“键值状态”(全量快照)。这个“追加命令 vs 状态快照”的对立是理解两种持久化的关键。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 1, + "tags": [ + "Redis", + "持久化", + "RDB" + ], + "question": "RDB 采用____(定时/每次写)方式做全量快照,因此文件____(更小/更大)、重载____(更快/更慢),但两次快照之间的数据在宕机后会____。", + "answer": [ + "定时", + "更小", + "更快", + "丢失" + ], + "answer_rule": "ordered", + "explanation": "RDB 定时全量快照:占用内存状态的极小二进制文件,恢复时直接载入状态而非逐命令重放,因此更小更快;代价是两次快照间隔内写入的数据在异常宕机时会丢失。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "AOF", + "AOF重写", + "状态快照", + "命令序列" + ], + "question": "AOF 重写(BGREWRITEAOF)的本质是把____序列重新生成为____描述,把命令流“压缩”成等价的键值状态;它丢弃的是被后续____覆盖的大量____命令。", + "answer": [ + "命令", + "键值状态快照", + "命令/写操作", + "中间" + ], + "answer_rule": "ordered", + "explanation": "重写的本质是“命令序列 → 状态快照”的等值转换:从当前内存状态出发生成重建所需的最小命令集,把被最终值覆盖的中间命令(如重复 SET、先增后删等)丢弃,从而压缩文件体积。注意四点顺序:命令、状态快照、中间命令。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "AOF", + "AOF重写" + ], + "question": "给定 AOF 命令序列:SET k 1;INCRBY k 5;SET k 9;DEL k;SET k 2。执行 AOF 重写后,恢复同样状态(k=2)的最小等价命令应为____(写出命令)。", + "answer": [ + "SET k 2" + ], + "answer_rule": "any", + "explanation": "前 4 条命令产生的任何状态都被最后的 SET k 2 完全覆盖,因此只需保留最后这条 SET。答案等价写法如 `SET k 2` 即可。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "AOF", + "binlog", + "追加日志", + "重放" + ], + "question": "Redis 的 ____ 与 MySQL 的 ____ 都属于记录写操作并可重放的追加日志;但 binlog 支持丰富的位点控制,而 AOF 没有原生的时间切片点,要做定点重放需要手动____文件。", + "answer": [ + "AOF", + "binlog", + "截断" + ], + "answer_rule": "ordered", + "explanation": "AOF 与 binlog 对应关系是“记录写操作并可重放”这一面;差别在于 binlog 具备 --start-position/--stop-position(精确)与 --start/--stop-datetime(近似)位点能力,AOF 则需人工截断 append 文件实现定点重放。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "binlog", + "重放" + ], + "question": "MySQL 基于 binlog 的 Point-In-Time Recovery(PITR)核心做法是:先用____恢复全量备份,再用 ____ 以 ____(精确)或 ____(近似)位点重放增量变更。", + "answer": [ + "全量备份/mysqldump", + "mysqlbinlog", + "--start-position/--stop-position", + "--start-datetime" + ], + "answer_rule": "ordered", + "explanation": "PITR 流程:先恢复最近的逻辑/物理全备,再回放备份之后的 binlog;精确恢复使用 mysqlbinlog 的 --start-position/--stop-position(字节位点),近似用 --start/--stop-datetime。因此顺序为:全量备份、mysqlbinlog、位置参数、datetime 参数。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "redo log", + "物理日志" + ], + "question": "InnoDB 的 redo log 记录的是对____(页面)层面所做的物理变更(页号、偏移、字节),属于____日志;AOF/ binlog 记录的是____层逻辑操作,属于____日志。", + "answer": [ + "物理页", + "物理", + "逻辑/命令/SQL/行变更", + "逻辑" + ], + "answer_rule": "ordered", + "explanation": "redo 面向物理页(页号+偏移+字节)描述最终状态变化,是物理日志;AOF 记录命令、binlog 记录 SQL/行变更,都在逻辑层,属逻辑日志。这是 redo 与 AOF/binlog 在“物理 vs 逻辑”层面的核心差异。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "redo log", + "环形缓冲", + "追加日志" + ], + "question": "InnoDB redo log 使用____大小的____结构,旧页写入磁盘后对应记录即可被循环____,因此它天然不会无限膨胀,无需像 AOF 那样进行显式____。", + "answer": [ + "固定", + "环形缓冲", + "覆盖/复用", + "重写/压缩" + ], + "answer_rule": "ordered", + "explanation": "redo 固定大小 + 环形复用(旧日志可被覆盖)实现隐式防膨胀;AOF 需显式重写(BGREWRITEAOF)合并命令。四空分别对应:固定、环形缓冲、覆盖、重写。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "AOF", + "redo log", + "AOF重写", + "环形缓冲" + ], + "question": "AOF 通过____方式应对日志膨胀(命令序列重新生成成语义快照),而 redo log 通过环形缓冲____旧页来隐式控制大小;两者防止膨胀的____不一致性正是“显式重写合并 vs 隐式覆盖”的体现。", + "answer": [ + "重写/重写合并", + "覆盖", + "时机" + ], + "answer_rule": "ordered", + "explanation": "AOF 日志会持续放大,需用户显式触发/策略配置自动触发的重写压缩;redo 则依靠环形覆盖机制在检查点后自动隐式回收。三空为:重写、覆盖、时机。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "redo log", + "undo log", + "一致", + "恢复" + ], + "question": "MySQL 崩溃恢复中,redo log 对所有____事务被脏页做前向____,undo log 对____事务做____,二者配合保证恢复后的持久性与一致性。", + "answer": [ + "已提交", + "重做", + "未提交", + "回滚" + ], + "answer_rule": "ordered", + "explanation": "崩溃恢复:已提交但落盘不全的事务用 redo 前向重做使其持久;未提交事务用 undo 回滚其部分写入。顺序为:已提交、重做、未提交、回滚。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/database/redis-persistence/meta.json b/topics/database/redis-persistence/meta.json new file mode 100644 index 0000000..30c75ae --- /dev/null +++ b/topics/database/redis-persistence/meta.json @@ -0,0 +1,57 @@ +{ + "slug": "redis-persistence", + "name": "Redis 持久化与日志对比", + "description": "AOF/RDB 持久化机制、AOF 重写压缩原理、与 MySQL binlog/redo log/undo log 的对比(数据结构角度)", + "tags": [ + "AOF", + "AOF重写", + "InnoDB成", + "MySQL", + "PITR", + "RDB", + "Redis", + "WAL", + "appendfsync", + "binlog", + "innodb_flush", + "position", + "redo", + "redo log", + "不膨胀", + "压缩", + "同步配置对比", + "对应关系", + "对比", + "持久化", + "持久性", + "日志", + "日志对比", + "时间点恢复", + "状态快照", + "环形", + "综合", + "覆盖", + "选型" + ], + "difficulty_range": [ + 2, + 4 + ], + "schema_version": "1.0.0", + "updated": "2026-09-07", + "question_files": [ + "single_choice", + "fill_blank", + "code_reading", + "short_answer" + ], + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "fill_blank": 10, + "code_reading": 5, + "short_answer": 10 + } + } +} diff --git a/topics/database/redis-persistence/short_answer.json b/topics/database/redis-persistence/short_answer.json new file mode 100644 index 0000000..6f8d9aa --- /dev/null +++ b/topics/database/redis-persistence/short_answer.json @@ -0,0 +1,261 @@ +{ + "topic": "redis-persistence", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "AOF", + "持久化", + "Redis" + ], + "question": "什么是 AOF 持久化?它记录『什么类型的信息』(命令还是数据状态),重启后如何恢复数据?", + "answer": "AOF(Append Only File)是 Redis 的持久化机制之一:每一条写命令(如 SET/LPUSH/INCRBY)以 RESP 协议格式追加写入一个 aof 文件中。它记录的是『写命令(操作)』,而不是『数据的最终状态』。重启恢复时,Redis 读取 AOF 文件,从第一条命令开始按顺序逐条重新执行(重放),把命令再次应用到空的内存数据库,最终恢复到崩溃/关闭前的一致状态。", + "keywords": [ + "写命令", + "追加", + "重放", + "RESP" + ], + "scoring_rubric": "①答出记录写命令而非数据状态得 2 分;②答出追加写入得 1 分;③答出重启后从头顺序重放命令恢复得 2 分。", + "explanation": "AOF 区别于 RDB 的本质:记录『操作』不记录『状态』,靠重放而非读快照。这是理解 AOF 重写、命令可压缩的基础。", + "source": null, + "related": [] + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "AOF", + "RDB", + "对比", + "选型" + ], + "question": "RDB 快照和 AOF 日志在数据安全性、恢复速度、文件大小三方面各有什么特点?如何取舍?", + "answer": "RDB(快照):存定时的全量快照,文件小、恢复快、加载快;但两次快照间隔若宕机,会丢失这段数据,安全性相对低。\nAOF(日志):存追加命令,数据更安全(丢失窗口可压到 appendfsync 粒度),但文件随命令增长、恢复需逐条重放所以较慢;自动重写可压缩体积。\n取舍:能容忍少量丢失、求恢复快文件小用 RDB;数据必须尽量不丢(如秒杀成交、订单)用 AOF 甚至 always;生产通常 RDB + AOF 混合兼顾。", + "keywords": [ + "RDB", + "AOF", + "安全性", + "恢复速度", + "文件大小", + "混合" + ], + "scoring_rubric": "①正确列出 RDB/AOF 在数据形式与安全性差异得 2 分;②恢复速度、文件大小差异得 2 分;③给出取舍得 1 分。", + "explanation": "RDB=状态快照、AOF=命令日志。安全、恢复速度、文件大小三维度对比是持久化选型的核心题。", + "source": null, + "related": [] + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "AOF重写", + "压缩", + "状态快照" + ], + "question": "从数据结构角度解释:AOF 为什么可『压缩』?重写到底改变哪些内容,本质是什么转换?", + "answer": "因为 AOF 是『命令序列』(log 结构),同一个 key 的多次写命令只有最后一次才决定其最终状态,早期命令基本可被覆盖/丢弃。例如 SET a=1 → SET a=2 → SET a=3 共存时,只有最后一条有意义;被 DEL 的 key 连任何命令都不需要。大量历史命令对恢复最终状态是冗余的,这才有压缩空间。\n\nAOF 重写(BGREWRITEAOF)的本质:不复用旧命令,而是遍历当前内存数据库的键值状态,为每个 key 重新生成一条『重建其最终值』的最小命令,生成一份新文件替换旧文件。这相当于把『命令序列 / 操作日志』等价转换为『键值状态快照』——丢弃所有可被最终状态覆盖的中间命令,文件从『随命令无限膨胀的命令日志』压缩成『接近最终状态的快照』。", + "keywords": [ + "命令冗余", + "只看最终值", + "状态快照", + "BGREWRITEAOF", + "遍历内存", + "最小命令" + ], + "scoring_rubric": "①说明 AOF 是命令序列、大量中间命令冗余可被覆盖得 2 分;②说出重写遍历当前内存为每个 key 生成最小重建命令、把命令序列转成状态快照得 3 分。", + "explanation": "这是『AOF 为何可压缩』的核心。关键在于『log(命令时间线)→ 状态快照』的数据结构切换,丢弃被覆盖的冗余命令使文件变小。", + "source": null, + "related": [] + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "binlog", + "AOF", + "对应关系", + "日志对比" + ], + "question": "常说 Redis 的 AOF 相当于 MySQL 的 binlog,这个类比哪里对、哪里不完全对?", + "answer": "对的地方:二者都是记录『写操作』的追加日志,都可以通过重放/执行日志恢复数据,都是持久化与复制的载体。\n不完全对的地方:\n1. 定位与服务对象——binlog 主要服务主从复制、逻辑备份、PITR(时间点恢复)、审计,是 Server 层、可跨引擎;AOF 主要服务 Redis 自身持久化与崩溃恢复。\n2. 时间点恢复能力——binlog 原生支持 --start-position/--stop-position(字节精确)与 --start/--stop-datetime(时间近似),可直接做 PITR;AOF 无原生精确到历史某个事务位置的时间切片重放工具,需要手动截断,所以『回到任意历史时刻』对 AOF 是近似/需手工的。\n3. 记录粒度——binlog 记录 SQL(statement)或行变更(row),而 AOF 记录命令(RESP)。", + "keywords": [ + "写操作日志", + "重放恢复", + "binlog", + "AOF", + "PITR", + "position", + "行变更", + "RESP" + ], + "scoring_rubric": "①答出两者都是写操作日志、可重放恢复得 2 分;②指出 binlog 服务主从/PITR、AOF 服务自身持久化得 1 分;③指出 binlog 原生支持 position 切片、AOF 需手动截断得 1 分。", + "explanation": "类比对在对『都是写操作日志、可重放恢复』,不完全对在服务对象与时间点恢复能力(binlog 位点精确、AOF 需手工)。", + "source": null, + "related": [] + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "binlog", + "redo log", + "日志", + "MySQL" + ], + "question": "MySQL 中 binlog 与 redo log 有什么区别?分别是『什么时候用』?", + "answer": "本质区别:\n- redo log:InnoDB 引擎内部自己用,记录物理页变更,目的是崩溃恢复、保证已提交事务数据不丢(WAL 重放);是固定大小环形缓冲。\n- binlog:MySQL Server 层产生,记录 SQL 或行变更,供主从复制、PITR、CDC、审计使用。\n\n什么时候用:关注崩溃数据不丢、恢复一致性 → 涉及 redo 机制与配置;主从复制、误删恢复到某时间点、把变更同步到外部系统(Canal 等)、审计 → 用 binlog。一句话:redo 是引擎内层『保底不丢』,binlog 是对外扩散/复制/审计层。", + "keywords": [ + "redo log", + "binlog", + "崩溃恢复", + "主从", + "PITR", + "Server层", + "物理页" + ], + "scoring_rubric": "①答出 redo 记录物理变更、引擎内用于崩溃后重放得 1 分;②答出 binlog 记录 SQL/行变更、用于主从/PITR/审计得 2 分;③说清两种场景分别用哪个日志得 2 分。", + "explanation": "redo 是引擎级崩溃恢复日志,binlog 是 server 级对外扩散日志,服务对象与场景截然不同的 ESR 是核心。", + "source": null, + "related": [] + }, + { + "id": "sa-006", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "redo", + "环形", + "不膨胀", + "覆盖" + ], + "question": "为什么 redo log 采用『固定大小环形缓冲』而不像 AOF 那样无限追加?覆盖不影响数据安全由什么保证?", + "answer": "redo log 是物理日志,只服务于崩溃恢复,因此容量可以固定:它采用固定大小的环形缓冲循环写入,写到末尾就回到开头覆盖最旧的记录,空间天然可控,不会像 AOF 那样随写入量无限膨胀,也就不需要 AOF 那种显式的重写压缩。\n\n覆盖之所以不影响数据安全,关键在于 checkpoint 机制:一条 redo 记录只有在它对应的脏页已经刷到磁盘之后才允许被覆盖——因为页已落盘,崩溃后不再需要这条 redo 来重做。checkpoint 会随脏页刷盘不断向前推进,推进点之前的空间才可复用。若写入速度过快、redo 快要追上 checkpoint,InnoDB 会强制推进刷脏页(表现为线上偶发的写入抖动),从而保证绝不覆盖尚未落盘的记录。", + "keywords": [ + "redo log", + "环形缓冲", + "固定大小", + "checkpoint", + "脏页刷盘", + "物理日志" + ], + "scoring_rubric": "总分 5 分:答出 redo 是固定大小环形缓冲、写满循环覆盖所以不膨胀、无需显式重写得 2 分;答出覆盖安全由 checkpoint 保证(对应脏页已刷盘才允许覆盖)得 2 分;答出 redo 追上 checkpoint 时会强制刷脏页得 1 分。", + "source": null, + "related": [], + "explanation": "redo 之所以不膨胀,根本在于它是物理日志且容量固定:环形缓冲写满就回到开头覆盖,空间天然可控。而覆盖不破坏安全性靠 checkpoint——只有对应脏页已刷盘,该 redo 记录才允许被覆盖,因为崩溃后已不需要它重做。若 redo 快追上 checkpoint,InnoDB 会强制刷脏页(表现为写入抖动)。" + }, + { + "id": "sa-007", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "binlog", + "AOF", + "PITR", + "时间点恢复", + "position" + ], + "question": "『可以回到有记录的任意时间节点』这句话,对 binlog 和 AOF 各成立到什么程度?请从精确性说明区别。", + "answer": "对 binlog:基本成立且是核心能力之一。binlog 每条事务都有文件号+offset(position),mysqlbinlog 支持 --start-position/--stop-position(字节精确),也可用 --start/--stop-datetime(时间近似)。配合全量备份可拼到准确的任意历史提交点;位置比时间更精确,并能停在一个完整事务边界。对 AOF:只能说『大致能回到某时刻,但需手动截断文件再重放』。AOF 本质是『从头到尾顺序重放』,命令没有内建可精确到某个事务边界的切片重放接口,因此要从历史某一刻恢复需人工截断,不是像 binlog 那样原生精确到 position 并重放到那儿。", + "keywords": [ + "binlog", + "AOF", + "PITR", + "position", + "手动截断", + "从头重放" + ], + "scoring_rubric": "①对 binlog 说明支持 position/时间切片、可精确 PITR 得 2 分;②对 AOF 说明默认从头到尾重放、缺原生精准切片、需要手动截断得 3 分。", + "explanation": "区别核心:binlog 有【文件+position】原生精确切片,AOF 是顺序重放日志、缺 position/时间片切入命令,需人为兜底。", + "source": null, + "related": [] + }, + { + "id": "sa-008", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "InnoDB成", + "WAL", + "redo log", + "持久性" + ], + "question": "什么是 WAL(Write-Ahead Logging)?MySQL InnoDB 为什么『先写 redo log 再落数据页』?", + "answer": "WAL(预写日志)核心:先提交前先把『本次修改对应的日志』写到磁盘,再做数据页修改。InnoDB 的做法:事务的 UPDATE 先记一条 redo 并 fsync 落盘(WAL),数据页先缓存内存 Buffer Pool,延迟刷盘;redo log fsync 之后 COMMIT 才算成功。好处:①顺序写比随机写快,redo 顺序 append、数据页随机刷,用顺序写低开销换持久性;②崩溃后重放 redo 把未落盘的已提交事务补上,保证失败不丢。本质:日志先行落盘,数据页延迟落盘,『已提交不丢』由日志而非页面落盘保证。", + "keywords": [ + "WAL", + "先写日志", + "Buffer Pool", + "顺序写", + "崩溃恢复一", + "持久性" + ], + "scoring_rubric": "①解释 WAL 是先写日志再写数据页得 2 分;②顺序写更快为何延迟刷得 1 分;③崩溃后重放 redo 保证已提交不丢得 2 分。", + "explanation": "WAL 是 InnoDB 持久性核心:COMMIT 只刷 redo log(顺序写),数据页由 Buffer Pool 管理、量通过同盘,崩溃重放。", + "source": null, + "related": [] + }, + { + "id": "sa-009", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "innodb_flush", + "AOF", + "appendfsync", + "同步配置对比" + ], + "question": "对比 MySQL innodb_flush_log_at_trx_commit 与 Redis appendfsync:各自取值与『安全 / 性能』取舍?", + "answer": "innodb_flush_log_at_trx_commit(MySQL):0=提交由系统定时批量刷,性能最高、进程崩溃可能丢最近数据;1(默认)=每次提交 fsync redo,最安全、性能最低;2=每次提交写 OS 写缓存不 fsync,由 OS 稍后刷,进程崩溃不丢但 OS 宕机丢,性能居中。\nappendfsync(Redis AOF):always=每条写命令 fsync,丢失最少、性能最差;every每秒=每秒 fsync,最多丢 1 秒、性能/安全平衡(默认);no=交给 OS,性能最好、安全最差。\n两者思想一致:用一个刷盘频率旋钮权衡『实时安全(慢)』与『批量(快)』。", + "keywords": [ + "innodb_flush_log_at_trx_commit", + "0/1/2", + "appendfsync", + "always/everysec/no", + "fsync" + ], + "scoring_rubric": "①列出 innodb_flush 三个值并说明取舍各等 1 分(共 2-3 分);②列出 append 三个值并取舍(2 分);③点明两者都是安全/性能权衡得 1分。", + "explanation": "二者分别对应 MySQL redo、Redis AOF 的 fsync 策略,核心是『每次提交是否立刻刷盘』带来的安全/性能权衡。", + "source": null, + "related": [] + }, + { + "id": "sa-010", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "AOF", + "binlog", + "redo", + "时间点恢复", + "综合" + ], + "question": "用『记录内容、主要使用者/场景起落、覆盖/追加结构、是否便于时间点恢复』四个维度对 AOF、binlog、redo log 做横向对比。", + "answer": "1. 记录内容:AOF 记 Redis 写命令(RESP);binlog 记 MySQL SQL(statement)或行变更(row);redo 记 InnoDB 物理页变更。\n2. 使用者/场景:AOF → Redis 持久化与崩溃恢复;binlog → 主从、PITR、CDC、审计(Server 层可跨引擎);redo → InnoDB 崩溃恢复、WAL。\n3. 记录结构:AOF 追加 append、可主动重写(压缩成状态快照);binlog 追加文件、靠 rotate+过期清理不主动压成快照;redo 环形覆盖已落盘记录、天然不膨胀也无需重写。\n4. 时间点恢复:AOF 从头顺序重放、无原生切片需手工截断;binlog 支持 position/时间、可直接做 PITR;redo 面向崩溃重放到当前、不用于历史时间点恢复。", + "keywords": [ + "AOF", + "binlog", + "redo log", + "记录内容", + "使用者", + "覆盖", + "环形", + "append", + "PITR" + ], + "scoring_rubric": "四维度各约等 1.25 分。要点:AOF 命令日志可显式重写、binlog 带 position 可用于 PITR、redo 物理环形覆盖用于崩溃重放。", + "explanation": "横向比较最综合。四组:记录内容、对象/场景、覆盖方式、时间点恢复能力。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/database/redis-persistence/single_choice.json b/topics/database/redis-persistence/single_choice.json new file mode 100644 index 0000000..55b1725 --- /dev/null +++ b/topics/database/redis-persistence/single_choice.json @@ -0,0 +1,229 @@ +{ + "topic": "redis-persistence", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-07T00:00:00+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 1, + "tags": [ + "Redis", + "持久化", + "AOF", + "RDB" + ], + "question": "Redis 的 AOF(Append Only File)与 RDB(快照)两种持久化方式,下列说法正确的是?", + "options": { + "A": "AOF 每隔固定时间做一次全量内存快照,文件小、重启加载快", + "B": "RDB 记录每条写命令文本,重启时依次重放命令来恢复数据", + "C": "AOF 通过记录每条写命令文本并在重启后重放来恢复数据;RDB 是定时全量快照", + "D": "RDB 与 AOF 都记录的是写命令序列,二者唯一的区别是文件格式文本/二进制" + }, + "answer": "C", + "explanation": "AOF(Append Only File)的本质是把每条写命令按序追加到文件,重启后从文件头部开始按顺序重放这些命令恢复内存数据;RDB 则是把某个时刻的完整键值对状态序列化成一个体积较小、重启加载更快的二进制快照文件,两次快照之间写入的数据在宕机时会丢失。A 把 AOF 说成快照、B 把 RDB 说成命令重放、D 声称二者都记录命令,均错误;C 准确描述了二者差异。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 1, + "tags": [ + "Redis", + "持久化", + "RDB" + ], + "question": "下列关于 RDB(快速文件快照)持久化的说法,错误的是?", + "options": { + "A": "RDB 生成的是某时刻内存键值状态的全量快照文件", + "B": "RDB 文件尺寸更小,重启后加载恢复速度远快于重放海量 AOF 命令", + "C": "RDB 采用定时触发,两次快照之间发生的写操作在宕机后会丢失", + "D": "RDB 会记录下每一条写命令,因此始终能恢复到任意一条指令级时刻" + }, + "answer": "D", + "explanation": "RDB 是内存状态的定时快照,只保存某个格式点时刻的全量数据,两次快照之间写入的数据在宕机时确实会丢失(C 正确)。其文件小、重载快(B 正确),保存的是状态本身而非命令序列(A 正确)。D 中“记录每一条写命令”是 AOF 的隐式,RDB 并无命令级别的恢复能力,因此 D 是错误说法。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "Redis", + "AOF", + "AOF重写", + "状态快照", + "命令序列" + ], + "question": "AOF 重写(Rewrite / BGREWRITEAOF)的核心本质是什么?", + "options": { + "A": "把追加文件里的命令逐条压缩成更短的命令,仍然是命令序列", + "B": "把命令序列重新生成为键值(状态快照)的等价描述,丢弃大量被最终值覆盖的中间命令,文件因此被“压缩”", + "C": "将 AOF 文件按时间切片,只保留最近一天的写命令", + "D": "把 AOF 与 RDB 混合份成一个文件,以便同时利用两者优势" + }, + "answer": "B", + "explanation": "AOF 重写的本质是“状态快照 vs 命令序列”的转换:它扫出当前内存里每个 key 的最终值,只生成恢复这些极简值所需的最小命令集(如直接 SET / RPUSH),把大量被后续命令覆盖的中间命令丢弃,从而大幅缩小文件。它并非简单的压缩/截断/混用 RDB,故 B 正确,A、C、D 均偏离本质。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "Redis", + "AOF", + "AOF重写" + ], + "question": "有一段 AOF 命令流:SET k 1;INCRBY k 5;DEL k;SET k 9。执行 AOF 重写后,恢复同样状态的最小等价命令序列最可能是?", + "options": { + "A": "SET k 1; SET k 6; DEL k; SET k 9", + "B": "SET k 9; INCRBY k 0", + "C": "只保留 SET k 9", + "D": "SETDEL: 只保留 DEL k" + }, + "answer": "C", + "explanation": "重写生成的是能重建当前内存状态的命令,mid 命令(SET k a、INCR k 5、DEL k)都被最终结果 k=9 覆盖/湮灭,因此只需保留末尾写 k=9 的 SET 即可。A、B 保留冗余中间命令不符合“最小等价”目标,D 错误因为最终 play 值应为 9 而非删除。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "AOF", + "binlog", + "追加日志", + "重放", + "复制" + ], + "question": "关于 Redis AOF 与 MySQL binlog 的对应关系,下列说法正确的是?", + "options": { + "A": "两者都只记录物理页的最终状态,且都天然支持到任意事务时刻的点恢复", + "B": "两者都属于记录写操作并可重放的逻辑追加日志,但 binlog 支持原生时间/位置切片点,AOF 没有,需要手动截断文件来定点重放", + "C": "AOF 是物理日志而 binlog 是逻辑日志,二者原理完全不同", + "D": "binlog 不具备重放能力,只是改库主从同步的审计文件" + }, + "answer": "B", + "explanation": "AOF 与 binlog 都按追加的方式,把写操作记录成可回放的重放日志(statement/row)后者。binlog 提供 --start-position/--stop-position 精确位点与 --start/--stop-datetime 近似时间来点恢复(PITR 核心工具);AOF 没有原生的按时间点切片重放能力,要回到过去某个时刻,通常需要手动根据 appendfile 做截断后再重放。B 正确。C 误把 AOF 当物理日志,D 假言 binlog 不可重放,均错误。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "AOF", + "redo log", + "binlog", + "物理日志", + "一致" + ], + "question": "相比 binlog 的 statement/row 逻辑格式,InnoDB 的 redo log 之所以是物理日志,是因为它记录的是?", + "options": { + "A": "每条 SQL 语句的完整文本,用于主从重放", + "B": "某个事务提交的行级前后镜像(before/after 值),用于回滚", + "C": "已提交事务可能修改过的页面、页号与偏移位置的增量变更,描述“物理页的最终状态变化”", + "D": "对数据库数据的完整状态快照(类 BTCA 快照)" + }, + "answer": "C", + "explanation": "redo log 记录的是对数据页物理层面的连续变更:哪个页(页号/偏移)、哪些字节被改成了什么,属于“逻辑日志”的物理状态。它不记录 SQL 文本(那是 binlog statement),不记录行 before/after 镜像(那是 undo log 的回滚图),也不做全量状态快照(RDB 的职责)。崩溃恢复依靠 redo 进行前向重做,C 正确。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "redo log", + "环形缓冲", + "追加日志" + ], + "question": "InnoDB 的 redo log 采用固定大小环形缓冲结构,因此它天然不会无限膨胀,其根本原因是?", + "options": { + "A": "它定期把日志导出到外部保存,磁盘满了就丢弃最旧日志", + "B": "环形固定大小,允许覆盖旧页,旧页写盘后再循环复用,旧记录被隐式覆盖弃——无需显式“压缩/重写”", + "C": "只允许写入固定字节数后强制关闭数据库要求人工清理", + "D": "InnoDB 每一条 redo 记录都写成后来会话内的最终状态,不再产生新日志" + }, + "answer": "B", + "explanation": "redo log 被设计成固定大小的循环(circular)缓冲:数据页在检查点后已写入磁盘,其对应日志就允许被覆盖,环形缓冲因此不断循环倒约 map 同一块存储空间,永远只占固定磁盘空间,不需要像 AOF 那样显式重写压缩(BG技术重写)。这就是“隐式覆盖” vs AOF“显式重写合并”的核心差别。B 正确。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "AOF", + "redo log", + "AOF重写", + "环形缓冲" + ], + "question": "关于 AOF 与 InnoDB redo log 在“日志膨胀控制”上的时机差异,正确的是?", + "options": { + "A": "两者都靠固定大小环形缓冲天然防膨胀,无需任何压缩", + "B": "redo 靠固定大小环形覆盖旧页隐式防止膨胀;AOF 是追加式命令日志,会持续放大,需要显式手动/触发 AOF 重写(BGREWRITEAOF)合并压缩", + "C": "AOF 一经开启就自动压缩命令,从不会膨胀;redo 反而会无限增长", + "D": "两者都靠定时全量快照替代全部日志才能防膨胀" + }, + "answer": "B", + "explanation": "AOF 追加记录的是命令序列,随写操作无限累积,只有通过显式重写(rewrite)把命令序列重实为等价的状态快照才能缩小文件;而 redo 位于固定大小的环形缓冲内,旧页写入盘后即可被覆盖复用,系统隐式防膨胀。因此“隐式覆盖 vs 显式重写合并”是二者膨胀控制时机的核心差异,B 正确。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "AOF", + "binlog", + "重放", + "互斥" + ], + "question": "要让 MySQL 通过 binlog 精确回放到某个事务提交的物理写入点,首选工具与参数是?", + "options": { + "A": "使用 mysqldump 全备份并重灌,配合 --secondary-drop-root", + "B": "用 mysqlbinlog --start-position/--stop-position 定位 binlog 文件位置精确回放,或用 --start/--stop-datetime 按时间近似回放", + "C": "直接重放 redo log 环形缓冲即可定位到任意事务", + "D": "仅靠 undo log 回滚未提交事务就能精确到物理点" + }, + "answer": "B", + "explanation": "binlog 是 MySQL 按位置(pos)与时间组织的事件流,mysqlbinlog 的 --start-position/--stop-position 可精确到事件字节位置,--start/--stop-datetime 提供近似时间点,是点恢复(PITR,Point-In-Time Recovery)核心工具。A 是全备份非精确位点;C 的 redo 是环形覆盖记录,只用于崩溃恢复前向重做,不能指定任意历史点;D 的 undo 只负责回滚未提交事务,B 正确。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "AOF", + "binlog", + "undo log", + "一致", + "恢复" + ], + "question": "MySQL 崩溃恢复时,redo log 与 undo log 的分工关系是?", + "options": { + "A": "只用 undo log 把已提交数据全部回滚到事务前状态", + "B": "redo log 对未写入物理页的已提交事务做前向重做,undo log 回滚尚未提交的事务,二者是物理重做/滚两阶段恢复的前提,配合保证恢复后一致性", + "C": "redo 用于逻辑回滚,undo 用于物理前向重做", + "D": "崩溃恢复完全依赖 AOF 重放命令,redo/undo 只在宕机前起作用" + }, + "answer": "B", + "explanation": "崩溃恢复遵循两条线索:对已经 commit 但脏页尚未落盘的事务,用 redo log 前向重做其物理页变更使其最终持久化;对尚未 commit 的事务,用 undo log 回滚已写入页中的部分改动,保证其最终被撤销。因此 redo 重做已提交 + undo 回滚未提交,配合完成恢复后的一致性与持久性,B 正确。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/index.json b/topics/index.json index e4f0ef9..d1eebf1 100644 --- a/topics/index.json +++ b/topics/index.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "updated": "2026-09-04", + "updated": "2026-09-07", "topics": [ { "slug": "qunar-ai-fullstack", @@ -287,6 +287,58 @@ } } ] + }, + { + "slug": "database", + "name": "数据库", + "description": "MySQL 与 Redis 核心原理:ACID 与三大日志、事务隔离级别、MVCC、Redis 持久化机制", + "subtopics": [ + { + "slug": "mysql-acid", + "name": "MySQL ACID 实现机制", + "description": "InnoDB 如何用日志与锁实现事务的原子性/一致性/隔离性/持久性:undo log、redo log、WAL、MVCC、锁", + "path": "topics/database/mysql-acid", + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "fill_blank": 10, + "code_reading": 5, + "short_answer": 10 + } + } + }, + { + "slug": "mysql-transaction-isolation", + "name": "MySQL 事务与隔离机制", + "description": "四种隔离级别、脏读/不可重复读/幻读、MVCC Read View 选版本逻辑、Repeatable Read 与 Next-Key Lock", + "path": "topics/database/mysql-transaction-isolation", + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "fill_blank": 10, + "code_reading": 5, + "short_answer": 10 + } + } + }, + { + "slug": "redis-persistence", + "name": "Redis 持久化与日志对比", + "description": "AOF/RDB 持久化机制、AOF 重写压缩原理、与 MySQL binlog/redo log/undo log 的对比(数据结构角度)", + "path": "topics/database/redis-persistence", + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "fill_blank": 10, + "code_reading": 5, + "short_answer": 10 + } + } + } + ] } ] }