想象你是一所大学食堂的厨师长。清晨六点,六位厨师同时开工:有人切菜,有人颠勺,有人往冷柜里补货。麻烦在于——每位厨师都能看到别人手上的半成品吗?一位厨师刚从冰箱拿出番茄还没洗,另一位就把它切了做沙拉;偏偏前者发现番茄不新鲜随手扔掉,后者的沙拉却已经端上了桌。数据库里的“事务”就是厨师手里的一道道菜,而“隔离级别”就是食堂制定的规矩:谁在什么时候,能看见什么样的食材状态。更高的隔离级别像更严的纪律,能挡住更多事故,却也让下厨节奏变慢。这篇文章将带你走进这间“数据库食堂”:先看清四种并发事故,再理解四档纪律的定义,最后钻入 MVCC 的“时间相册”——版本链与快照,掌握如何为业务选出最合适的隔离级别。
隔离级别是数据库并发安全的“交通规则”——它是面试高频考点、线上事故排查的必备视角,也是 AI 工程师把特征、标签、元数据安全落库的基础功。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
事务基本概念
想象你管理着一所大学食堂的厨房。凌晨五点半,六位厨师同时开工,每个人手头都攥着一道菜:有人煎蛋,有人熬粥,有人切水果。你作为厨师长,定下了一条铁规矩:任何一道菜,要么从备料到装盘完整地端出厨房,要么在发现问题时整锅倒掉重做,绝不允许“做了一半的菜”留在灶台上。这条规矩保证了大家吃到的每样东西都是完整、卫生的。
数据库世界里的“事务”(Transaction),正是这样一道“菜”。它把一组读取和写入数据库的操作打包成一个整体:要么全部执行成功并保存(提交 Commit),要么全部不生效(回滚 Rollback)。转账就是最经典的例子——扣款和入账必须同生共死,绝不能出现“钱扣了但对方没收到”的半个操作。
为了确保“菜”的质量,事务必须满足四大特性,合称 ACID。原子性(Atomicity)保证操作全做或全不做;一致性(Consistency)保证数据在事务前后都符合业务规则;隔离性(Isolation)保证并发事务互不干扰;持久性(Durability)保证事务一旦提交,即使断电数据也不会丢。四者相辅相成,而隔离性正是本文的主角——它决定了多个厨师(事务)在共享厨房(数据库)里能互相看到什么。
从你喊出“开工”到顾客吃到热腾腾的早餐,每一道菜都经历着完整的生命周期。事务也是如此:从 BEGIN 开启,到若干读写操作,再到 COMMIT 或 ROLLBACK 收尾。理解了“一道菜”的边界,我们才能继续讨论:当多道菜同时下厨时,厨师们会碰撞出哪些“事故”。
- T 表示一个事务事务是数据库操作的最小工作单元,类似一道菜的完整制作流程,任何半成品状态都不允许对外暴露。
- r(x) 与 w(x) 代表读写读取操作 r(x) 拿到某个数据的值,写入操作 w(x) 修改它的值,一个事务可以包含若干这样的操作。
- COMMIT 提交全部操作成功验证后,一次性写入磁盘生效,就像菜肴通过验收端上桌。
- ROLLBACK 回滚任一环节出错,撤销全部修改,回到事务开始前的状态,等于把半成品倒进垃圾桶重来。
并发异常问题
厨房越来越忙,问题来了。六个厨师共享一块砧板、一口炒锅和一台冰箱。A 厨师正把西红柿从冰箱里拿出来准备清洗,B 厨师伸手就把它切了做沙拉——结果 A 发现西红柿不新鲜,顺手扔了,B 的沙拉却已经端上了桌。这就是“用了一个即将被丢弃的半成品”。更糟的是,同样的碰撞还有三种:同一个厨师隔五分钟看同一颗白菜,一次完整、一次只剩半颗;洗碗工数盘子,第一次 100 个、第二次 103 个;两个人同时改菜谱,后写的把先写的覆盖了。
这些场景在数据库里对应着四种大名鼎鼎的并发异常。第一,脏读(Dirty Read):一个事务读到另一个未提交事务的修改,而后者随后回滚,前者读到的就是“脏数据”——数据从未真实存在过。第二,不可重复读(Non-repeatable Read):同一事务内两次读取同一数据,值却不同,因为另一事务在这期间提交了修改。第三,幻读(Phantom Read):同一事务内两次查询同一范围,记录条数变了,另一事务插入或删除了新行。第四,更新丢失(Lost Update):两个事务同时读改写同一数据,后提交的覆盖了先提交的,导致一个更新凭空蒸发。
四种事故中,脏读最致命也最基础,因为它直接破坏了“已提交数据才可信”的底线。其他三种则在不同维度上挑战一致性。值得注意的是,更新丢失在 SQL 标准中并未与隔离级别直接挂钩,但它同样频繁出现在真实事故里,尤其在“读改写”(read-modify-write)这种经典业务模式中。
这四种事故,就像贴在厨房墙上的安全事故清单。把它们一一列清楚,接下来要制定的“厨房纪律”——隔离级别——就有了明确的靶子:每档纪律能防住哪几种事故,一目了然。
- T_1 写入 x 但未提交T1 修改了数据 x,但还没验收(COMMIT),此时修改是临时的、随时可能作废。
- T_2 读到 xT2 在 T1 提交之前就读取了 x,它只知道 x 的当前值,不知道这个值“还没盖章定稿”。
- T_1 回滚T1 最终放弃修改,x 还原为旧值,T2 之前读到的值成了从未存在过的“脏数据”。
隔离级别定义
为了管理共享厨房的风险,你制定了四档“纪律”。第一档“随便看”:厨师们可以随时看任何人的半成品,哪怕对方还没决定要不要用;第二档“看成品”:只能看别人已端上桌的菜,锅里的半成品一概不准碰;第三档“开饭前定菜单”:一旦开始做自己的菜,你看到的食材状态就锁定不变,别人之后怎么折腾都与你无关;第四档“一人一灶”:所有人排队做菜,做完一个再来一个,彻底没有同时下厨这回事。
这四档纪律,正是 SQL 标准定义的四种隔离级别:读未提交(Read Uncommitted,RU)、读已提交(Read Committed,RC)、可重复读(Repeatable Read,RR)和可串行化(Serializable,S)。隔离级别完整地定义了一个事务能看到其他事务的哪些修改:RU 完全不隔离,所有临时修改都可见;RC 只允许读取已提交的数据,脏读被避免;RR 保证事务内多次读同一数据结果一致,不可重复读被避免;S 是最高级别,事务像排队一样执行,幻读也被禁止。
从工程视角看,隔离级别本质是一份“可见性合同”:级别越低,约定越宽松,事务看到的数据越“新鲜”但也越不可靠;级别越高,约定越严格,事务看到的数据越稳定但并发能力越差。选择哪档,就是选择用多少性能换多少一致性。
纪律定好了,问题也随之而来:“看成品”和“菜单锁定”说起来容易,数据库到底怎么实现?答案就是 MVCC——一场聪明的时间管理艺术,它让读操作几乎不阻塞写操作,写操作也不阻塞读操作。
- RU 读未提交最低级别,事务能读其他事务尚未提交的修改,饿极了什么都能看,四种异常全可能发生。
- RC 读已提交只能读到已提交的数据,脏读被拦截,但同一事务内两次读可能结果不同。
- RR 可重复读事务首次读取时的快照被长期复用,同一数据多次读取保持一致,不可重复读被杜绝。
- S 可串行化所有事务近似串行执行,相当于食堂里只有一个厨师,幻读也被禁止。
MVCC核心机制
假设你要求每位厨师开工前先拍一张食材全景照(快照),之后整个做菜过程中,无论其他厨师怎么折腾食材,你都只认自己照片里的样子。不仅如此,每次有人修改食材,你并不扔掉旧的,而是在旁边贴上一张写着“修改人编号”的新照片,再用一根线把新旧照片串起来——这就是“版本链”。
MVCC(Multi-Version Concurrency Control,多版本并发控制)正是这个思路:每一行数据被修改时不会覆盖旧值,而是生成一个新版本。每个版本都带两个隐藏列:trx_id 记录最后修改该版本的事务编号,roll_pointer 是回滚指针,指向上一个版本。这些版本通过指针串成一条链条,读取数据时从最新往最旧找,用 ReadView(读视图)判断哪个版本对自己可见。
ReadView 在快照读(普通 SELECT)时创建,记录三样关键信息:所有尚未提交事务的编号集合 m_ids、其中最小编号 min_trx_id、以及系统下一个待分配的编号 max_trx_id。判断版本可见性的规则可以归纳为四句话:版本编号在 m_ids 里,说明修改者还没下班,不可见;编号大于等于 max_trx_id,说明是快照之后才开启的“未来事务”,不可见;编号小于 min_trx_id,说明在快照之前就已提交,可见;编号落在中间区间且不在 m_ids 里,说明快照创建时它已提交,可见。
有了 ReadView 这把“时间标尺”,RC 和 RR 的实现差异立刻清晰了:RC 每次读取都重新拍一张合影(新建 ReadView),所以能看到别人最新提交的修改;RR 整场做菜只拍一张合影(复用 ReadView),所以“不可重复读”自然被杜绝。这就是 MVCC 的精髓——用版本和时间,而不是用锁,换来高并发下的读安全。
- m_ids 活跃事务集合创建快照那一刻尚未提交的所有事务编号,相当于“合影时还在厨房里的人”。
- trx_id ∈ m_ids → 不可见数据仍被某个没下班的事务攥在手里,它可能随时回滚,绝对不能信。
- trx_id ≥ max_trx_id → 不可见这个事务在快照之后才开启,是“未来的人”,它写的东西当然不能出现在过去。
- trx_id < min_trx_id → 可见事务编号比所有活跃事务都小,说明它早已提交并下班,数据安全可用。
- 中间区间且不在 m_ids → 可见编号在快照区间内但不在合影名单上,说明它当时已提交,可以放心使用。
隔离级别对比
四档纪律摆在面前,你该选哪一档?决策的关键,是画一张“事故防护对照表”,看看每档纪律到底守住了什么、漏掉了什么。食堂如此,数据库亦然。把四种并发异常与四种隔离级别对应起来,就得到了那张著名的对照表。
对照表揭示了一个清晰的规律:每升一级,多堵住一种异常,也多付出一分代价。RU 档位不设防,四种异常全可能发生,实践中几乎不用;RC 档位挡住了脏读,但不可重复读和幻读仍可能发生;RR 档位在 SQL 标准下仍不防幻读,但 MySQL InnoDB 通过 MVCC 快照读加间隙锁,在绝大多数场景下连幻读也一并堵住了;S 档位全副武装,所有异常都被禁止,代价是并发度急剧下降,吞吐量可能只有 RC 的几分之一。
理解这张表,还要把握一对关键矛盾:一致性(Correctness)与性能(Performance)的此消彼长。隔离级别越低,版本链遍历和锁等待越少,吞吐量越高,但数据可信度越低;级别越高,每次读取要做的可见性判断越多,写操作要穿的“锁门”越多,系统越安全也越慢。大多数生产事故,恰恰发生在工程师误以为“默认级别就够了”的时刻。
对照表贴出来后,不同食堂能找到自己的位置:早点铺子客人多、菜品简单,选 RC 就够了;高档西餐厅追求极致一致,宁可排队也要 S。回到你的厨房,“看成品”通常最均衡,“开饭前锁定”适合对一致性要求更高的订单。
- RU 行——全不设防读未提交档位不隔离任何并发修改,三种异常都可能发生,只适合对数据可信度无要求的场景。
- RC 行——挡住脏读读已提交档位每次读都新建快照,未提交版本不可见,但快照不复用导致两次读取结果可能不同。
- RR 行——重复读一致快照复用让重复读取保持一致,但按 SQL 标准幻读仍可能发生;MySQL 靠 MVCC 加间隙锁使其实际也避免幻读。
- S 行——全副武装可串行化把所有事务排队执行,三种异常全部杜绝,是“宁可慢也要对”的终极方案。
实现机制差异
同样是“看成品”或“菜单锁定”的纪律,不同食堂的执行方式截然不同。有的食堂靠“拍照存档”(MVCC),有的靠“锁门挂牌”(锁机制),有的两者混用。走进三家知名“食堂”——MySQL、PostgreSQL、Oracle,你会发现它们对同一份菜单的理解各有千秋。
MySQL InnoDB 默认使用 RR,这是它与 SQL 标准推荐 RC 的最大差异。InnoDB 的 RR 之所以敢拍胸脯说防幻读,靠的是“双轨制”:普通 SELECT 走 MVCC 快照读,不加锁;UPDATE、DELETE、SELECT …… FOR UPDATE 走当前读,加记录锁和间隙锁。间隙锁锁住查询范围中的“空隙”,阻止其他事务在范围内插入新行,幻读被彻底堵死。PostgreSQL 默认 RC,同样用 MVCC,但它的 RR 靠真正的全局快照(快照隔离)实现,不引入间隙锁,性能模型完全不同。Oracle 则只提供 RC 和 S 两档(其余映射到 RC),用 undo 回滚段构建一致性读,读操作全程不加读写互斥锁。
三家的共同点是:读取靠 MVCC,写入靠锁,只是分工比例不同。理解这个差异,对跨数据库迁移和踩坑排查至关重要——在 MySQL 上“从来没事”的 RR 语义,迁到 PostgreSQL 或 Oracle 后可能行为大变。
了解三家食堂的“后厨机密”,你在选型时就能超脱“隔离级别”的表象:MySQL 的 RR 之所以出名,是因为它用 MVCC 换来了“既严格又高效”;PostgreSQL 的 RC 更“诚实”,把范围的锁留给了用户;Oracle 则用回滚段的魔法让读永远不堵写。没有最好的食堂,只有最合适的后厨。
- MVCC 快照读普通 SELECT 语句直接从版本链中挑可见版本,不加锁,并发性能极高。
- 记录锁对命中的行加锁,防止其他事务同时修改或删除这些行,解决写写冲突。
- 间隙锁对查询范围中不存在的“空隙”加锁,阻止其他事务插入新行,从而堵住幻读。
工程实践选型
菜谱终于齐了。现在的问题是:你的食堂到底该执行哪档纪律?人流稀少的深夜食堂,“一人一灶”完全可行;可到了中午十二点的大学食堂,如果还让所有人排队做菜,学生们怕是要揭竿而起。纪律必须匹配客流量和菜品类型,隔离级别的选择也必须匹配业务的一致性需求和并发特性。
工程师选隔离级别,没有万能答案,但有清晰的决策路径。第一步,识别业务对一致性的敏感度:资金、库存、票务这类“错一分钱都是事故”的系统,关键操作必须上 RR 甚至 S,且常用 SELECT …… FOR UPDATE 做当前读加锁;内容社区、商品详情、日志统计这类读多写少、允许轻微抖动的系统,选 RC 或数据库默认级别即可。第二步,评估并发压力:高并发写入场景下,S 的锁等待会让吞吐量断崖下跌,应尽量用 MVCC 快照读消化读请求。第三步,做好兜底监控:观察锁等待超时、死锁日志和全表扫描,找出热点行,用拆分、乐观锁版本号或幂等键化解冲突。
性能调优层面还有几个实用技巧:优先使用 RR 加 MVCC 快照读,把普通查询彻底变成无锁操作;对只读事务显式标记为 read-only,减少无谓的锁持有;把大事务拆小,缩短锁的持有时间;用 read replica 分担快照读流量,让主库专心处理当前读。
夜深了,食堂打烊。你站在后厨门口回看这一天:从“一道菜”的边界,到四种“事故”,再到四档“纪律”,最后深入 MVCC 的“时间魔法”——数据库并发安全的全貌,其实不过是一间共享厨房的管理哲学。而你的菜(数据)之所以万无一失,是因为你既懂纪律,更懂纪律背后的机制。
- 一致性风险成本脏读、不可重复读、幻读一旦发生,可能带来数据错乱、业务故障甚至资损,代价是概率与损失的乘积。
- 并发性能损耗隔离级别越高,锁等待、死锁重试、版本链遍历的开销越大,吞吐量越低。
- 总成本最小化选型综合两边成本找到均衡点——多数业务落在 RC 或 RR,只有强一致场景才升级 S。
AI 应用场景
特征平台的实时特征更新
阶段:特征工程 / 在线推理
解决的问题:推荐系统需要实时更新用户特征(点击、收藏、加购),多个服务并发写入同一张特征表。传统方式没有事务保护时,脏读会导致召回服务读到“用户刚加购又删除”的中间态特征,推荐结果剧烈震荡。利用 RC/RR 隔离级别加 MVCC,召回服务只读到已提交的最终特征,排序结果稳定可靠。
具体案例:某电商推荐平台,用户行为日志服务以每秒上千次写入更新特征表。上线前使用 RU 级别,脏读导致线上 CTR 抖动 15%;切换为默认 RR 后,召回服务通过快照读拿到一致的已提交特征,推荐排序恢复稳定。
AI 训练数据的标注管道
阶段:数据准备 / 数据清洗
解决的问题:多 worker 并发更新样本标签时,若两个 worker 同时读改写同一条样本的标签,会发生更新丢失。传统“最后写入者胜”的粗暴策略会让先提交的标注被无意识覆盖。利用 RC 级别配合 SELECT …… FOR UPDATE 当前读,对样本行加锁,确保同一时刻只有一个 worker 能改一条样本。
具体案例:某标注平台 50 个 worker 并发处理百万级样本。使用 RC + 当前读后,重复标注冲突率从 3.2% 降到 0.01% 以下;后台质检任务用 RR 快照读,保证一次巡检看到一致的标注版本,避免了指标前后矛盾。
模型实验元数据管理
阶段:模型训练 / MLOps
解决的问题:多个实验并发写模型指标表,若发生脏读,实验对比看板会展示未提交的中间指标,导致优劣判断失准。利用 RC 级别保证看板只读到已提交的完整指标;用 RR 会话生成训练报告时,多次查询结果一致,避免同一指标在报告中前后矛盾。
具体案例:某训练平台每晚并发跑 200 个实验,每个实验的指标分阶段写入元数据库。配置 RC 后,看板展示的 loss、AUC 等指标全部来自已提交的事务;同时把报告生成会话设为 RR,确保一份报告内的数据出自同一时间快照。
Python 演示
class MVCCDatabase: """简易 MVCC 模拟器:版本链 + ReadView 快照读""" def __init__(self): self.chains = {} # key -> 最新版本节点 self.next_id = 1 # 下一个待分配事务编号 self.running = set() # 活跃(未提交)事务编号 def begin(self): tid = self.next_id self.next_id += 1 self.running.add(tid) return Transaction(tid, self) def commit(self, trx): self.running.discard(trx.tid) def write(self, trx, key, value): node = { 'trx_id': trx.tid, 'value': value, 'roll_pointer': self.chains.get(key) } self.chains[key] = node trx.writes[key] = node return node def read(self, trx, key): if trx.snapshot is None: # RR: 首次读创建快照并一直复用 trx.snapshot = { 'min': min(self.running), 'max': max(self.running) + 1, 'active': set(self.running) } node = self.chains.get(key) while node is not None: if self._visible(node['trx_id'], trx): return node['value'] node = node['roll_pointer'] # 沿版本链找更旧的版本 return None def _visible(self, trx_id, trx): if trx_id == trx.tid: return True # 自己写的一定可见 snap = trx.snapshot if trx_id < snap['min']: return True # 快照创建前已提交 if trx_id >= snap['max']: return False # 快照之后的未来事务 return trx_id not in snap['active'] # 区间内且已提交 class Transaction: def __init__(self, tid, db): self.tid = tid self.db = db self.snapshot = None # ReadView,RR 模式下复用 self.writes = {} def read(self, key): return self.db.read(self, key) def write(self, key, value): return self.db.write(self, key, value) if __name__ == '__main__': db = MVCCDatabase() t1 = db.begin() t1.write('balance', 100) t2 = db.begin() print('脏读检查: t2 读到', t2.read('balance')) # None db.commit(t1) t3 = db.begin() print('提交后: t3 读到', t3.read('balance')) # 100 db.commit(t3) t4 = db.begin() print('RR 快照: t4 读到', t4.read('balance')) # 100 t5 = db.begin() t5.write('balance', 200) db.commit(t5) print('RR 复用: t4 再读', t4.read('balance')) # 仍是 100 db.commit(t4)
'roll_pointer': self.chains.get(key)新版本的回滚指针指向当前最新版本,串成一条版本链if trx.snapshot is None:RR 模式下整个事务只创建一次 ReadView,之后一直复用if self._visible(node['trx_id'], trx):从版本链头部开始逐个判断版本可见性node = node['roll_pointer']版本不可见就沿着回滚指针跳向更旧的版本if trx_id == trx.tid:自己事务写入的版本永远对自己可见if trx_id < snap['min']:编号小于快照最小活跃编号,说明早已提交return trx_id not in snap['active']区间内且不在活跃集合,即为已提交的可见版本
常见误区
误区我用的 MySQL 默认可重复读,从来没见过幻读,所以 SQL 标准的可重复读就是能防幻读的,所有数据库都一样。
正解SQL 标准中,RR 只保证同一事务内多次读同一行结果一致,并不防止幻读。MySQL InnoDB 的 RR 之所以防幻读,是 MVCC 快照读 + 当前读间隙锁的组合拳特例。PostgreSQL 的 RR 快照隔离同样防幻读,但 Oracle 没有真正的 RR 档,它把 RR 映射为 RC。脱离具体数据库谈隔离级别,等于脱离食堂谈菜谱。
为何常见:大多数开发者只接触过 MySQL,把 InnoDB 的行为当成了通用标准;面试写“RR 不防幻读”又和直觉打架,容易自我怀疑。
误区MVCC 就是每个事务一个快照,读自己的旧版本,不产生锁竞争,所以 MVCC 可以完全替代锁机制。
正解MVCC 只对快照读(普通 SELECT)无锁;对当前读(UPDATE、DELETE、SELECT …… FOR UPDATE)仍然必须加锁,否则两个事务同时修改同一行必然产生更新丢失。MySQL RR 下当前读还要加间隙锁来防幻读。MVCC 和锁是“双轨制”:读走时间线,写走锁。
为何常见:很多教程强调“MVCC 解决读写阻塞”,让人误以为 MVCC 消灭了所有锁,忽略了写-写冲突始终需要锁协调这一事实。
误区数据安全第一,把隔离级别设成最高的 Serializable,什么问题都不会有,反正机器性能足够。
正解S 级会用锁和串行化执行,锁等待与死锁概率显著上升,吞吐量可能跌到 RC 的几分之一。多数业务在 RC 或 RR 下配合业务逻辑(如乐观锁版本号、幂等键)就能满足需求。隔离级别是工具不是保险,过度的隔离会惩罚所有请求;选型应遵循“总成本最低”原则。
为何常见:新手容易把“保守”当作“安全”,忽略了并发系统的本质是权衡;缺少线上压测数据支撑,凭感觉定了最高档。
误区既然 MVCC 都是靠 ReadView 判断可见性,RC 和 RR 都是“读快照”,不就是同一个东西吗?
正解关键差异在 ReadView 的复用策略:RC 每条 SELECT 语句都新建 ReadView,所以能看到其他事务最新提交的修改;RR 整个事务只复用第一个 ReadView,后续读永远停留在事务开始时的“合影”。这正是不可重复读在 RR 下被消除、在 RC 下仍存在的原因。
为何常见:只学了 ReadView 的判定规则,没学它何时创建和复用;把“读快照”当成单一行为,忽略了快照的生命周期。
面试题
Q1. 事务的 ACID 特性中,“隔离性(Isolation)”主要解决什么问题?
- A.保证事务内操作全做或全不做
- B.防止并发事务相互读到中间状态← 正确答案
- C.保证事务提交后数据持久不丢
- D.保证事务前后数据满足业务约束
答案:B。隔离性关注多个事务并发执行时对彼此的可见性,防止读到未提交的中间状态。A 是原子性,C 是持久性,D 是一致性。就像厨房里定规矩:别人没端上桌的菜你绝不能动。
Q2. 以下哪种情况属于“脏读”?
- A.读到另一个事务已提交的数据
- B.读到另一个事务未提交的数据← 正确答案
- C.同一事务两次读取结果不同
- D.同一事务两次查询行数不同
答案:B。脏读的核心特征是“读到未提交的数据”,且该事务随后回滚,数据作废。C 是不可重复读,D 是幻读,A 是正常读操作。相当于你拿走了别人正准备扔掉的菜。
Q3. MySQL InnoDB 引擎的默认隔离级别是?
- A.读未提交(RU)
- B.读已提交(RC)
- C.可重复读(RR)← 正确答案
- D.可串行化(S)
答案:C。MySQL InnoDB 默认 RR,这是它与 SQL 标准推荐 RC 的最大差异。RU 几乎不用,S 性能太差很少直接选。这就像食堂默认执行“开饭前锁定菜单”的纪律。
Q4. 读已提交(RC)级别下,仍然可能发生哪种异常?
- A.读取到未提交的脏数据
- B.两个事务相互覆盖更新← 正确答案
- C.事务提交后数据永久丢失
- D.事务执行到一半即失败
答案:B。RC 只保证读到已提交数据,脏读被避免,但两个事务同时读改写同一行时,后提交者会覆盖先提交者,形成更新丢失。A 在 RC 下不可能,C 属于持久性问题,D 属于原子性问题。
Q5. ReadView 中的 m_ids 集合记录的是什么?
- A.所有已提交事务的编号集合
- B.创建快照时活跃事务的编号集合← 正确答案
- C.系统全部历史事务的编号集合
- D.正在执行写操作事务的编号集合
答案:B。m_ids 是 ReadView 创建那一刻尚未提交的事务编号集合,用于判断某版本是否由“仍在厨房的人”修改。已提交事务不会出现在 m_ids 中,已提交与否还要结合 min/max 编号区间判断。
Q6. 下列关于可重复读(RR)的说法正确的是?
- A.SQL 标准中 RR 能完全避免幻读
- B.RR 保证事务内重复读同一行结果一致← 正确答案
- C.RR 是四种隔离级别中最高的档位
- D.RR 的实现中完全不使用任何锁
答案:B。RR 保证同一事务内重复读取同一行结果一致。SQL 标准中 RR 不防幻读(A 错),最高级别是 S(C 错),RR 仍需配合锁处理写冲突(D 错)。MySQL 的 RR 防幻读是特例。
Q7. MySQL InnoDB 在 RR 级别下,下列哪种语句会使用当前读并加锁?
- A.普通 SELECT 查询语句
- B.加锁 SELECT FOR UPDATE← 正确答案
- C.事务开始的 BEGIN 语句
- D.数据库版本信息查询
答案:B。SELECT …… FOR UPDATE 是“当前读”,会对命中的行加排他锁;普通 SELECT 走 MVCC 快照读不加锁;BEGIN 只是开启事务;版本查询不涉及行数据。当前读是防幻读的第二条轨道。
Q8. 在读未提交(RU)级别下,可能发生哪些并发异常?(多选)
- A.脏读(读到未提交数据)← 正确答案
- B.不可重复读(两次结果不同)← 正确答案
- C.幻读(范围行数变化)← 正确答案
- D.以上三种都不可能出现
答案:ABC。RU 档位不设防,三种异常全部可能发生,D 是反向干扰项。RU 的唯一好处是读取无延迟、无锁等待,但数据可信度极低,实践中几乎不用。
Q9. 关于 MVCC 版本可见性判断,下列说法正确的有?(多选)
- A.版本编号小于 min_trx_id 时可见← 正确答案
- B.版本编号在 m_ids 活跃集合中可见
- C.版本编号大于等于 max_trx_id 不可见← 正确答案
- D.区间内且不在 m_ids 时可见← 正确答案
答案:ACD。m_ids 是活跃事务集合,编号在其中说明该事务还没提交,对应版本不可见(B 错)。小于 min_trx_id 说明早已提交可见;大于等于 max_trx_id 是未来事务不可见;区间内不在 m_ids 则已提交可见。
Q10. 关于不同数据库隔离级别实现,下列说法正确的有?(多选)
- A.MySQL InnoDB 默认隔离级别为 RR← 正确答案
- B.PostgreSQL 默认隔离级别为 S
- C.Oracle 默认隔离级别为读已提交 RC← 正确答案
- D.MySQL RR 完全不需要任何锁机制
答案:AC。InnoDB 默认 RR;Oracle 默认 RC,用 undo 构建一致性读;PostgreSQL 默认也是 RC(B 错);MySQL RR 防幻读需要 MVCC 快照读加当前读间隙锁两者配合,并非完全无锁(D 错)。