515acbcc7f
Deploy Examination / deploy (push) Successful in 4s
- mysql-acid (35): undo/redo log, WAL, 崩溃恢复三阶段, 两阶段提交 - mysql-transaction-isolation (35): 四隔离级别, 三异常, Read View, 快照读/当前读, next-key lock - redis-persistence (35): RDB/AOF/混合持久化, AOF 重写, vs binlog/redo log - 每子主题: single_choice×10 + fill_blank×10 + short_answer×10 + code_reading×5 - 全部通过 question.schema.json 校验
257 lines
14 KiB
JSON
257 lines
14 KiB
JSON
{
|
||
"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 及以上级别的普通读不读未提交数据,不发生脏读。"
|
||
}
|
||
]
|
||
} |