{ "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 及以上级别的普通读不读未提交数据,不发生脏读。" } ] }