想象一下,你们六个人合租一套公寓,只有一个公共厨房。每天晚饭时间,每个人都想用厨房:小王在切菜,小李在炒菜,小张盯着烤箱里的蛋糕。你们共享的不仅是灶台和案板,还有冰箱里的食材。如果没有任何规矩,就可能发生:小李顺手拿走小王刚切好、还没决定要不要用的洋葱;小张看到冰箱里只剩下半瓶牛奶,转头却被小王倒进了自己的咖啡——每个人眼里的厨房都不一样,混战在所难免。 数据库里的并发事务就是这个厨房。多个事务同时读写同一批数据,就像多个室友同时用一个厨房。如果数据库不制定“规约”,事务之间就会互相踩踏,读到彼此写到一半的数据,出现各种奇怪现象。SQL标准为此定义了四套“厨房使用规范”——隔离级别,从最宽松的“读未提交”到最严格的“串行化”。本文就用这个合租厨房的故事,带你彻底搞懂四种隔离级别、各自的副作用,以及MySQL等数据库保障性能的秘密武器——MVCC多版本并发控制。

岗位关联

隔离级别与MVCC是理解数据库并发行为的基石。掌握它们,你能快速定位线上“奇怪数据”问题,写出高并发下依然正确的事务代码,这是后端开发者和DBA必备的硬核技能。

目录
  1. 可视化演示
  2. 并发问题背景
  3. 读未提交
  4. 读已提交
  5. 可重复读
  6. 串行化
  7. MVCC原理
  8. 级别选择
  9. 实战排查
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

可视化展示四种隔离级别(RU/RC/RR/SR)对脏读、不可重复读、幻读的防护能力,直观对比MVCC多版本并发控制机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

隔离级别矩阵 RU防止0种问题,RC防脏读,RR防脏读+不可重复读,SR全防 隔离强度:RU < RC < RR < SR MySQL默认RR级别,PostgreSQL默认RC级别

并发问题背景

合租公寓的厨房之所以能运转,靠的是一本《公寓管理规约》。规约规定了四条底线:做饭必须一气呵成,不能做一半就丢下不管;做出来的菜必须靠谱,不能把厨房搞成一锅乱炖;每个人用厨房时不能随意干扰别人;今天分配好的食材,明天依然有效。这四条底线,正是数据库事务ACID四大特性的生活版。

在数据库里,ACID分别指原子性(Atomicity,事务不可分割地全部成功或全部失败)、一致性(Consistency,事务前后数据始终合法)、隔离性(Isolation,并发事务之间互不干扰)、持久性(Durability,事务提交后数据不丢失)。四者之中,隔离性最难实现:因为多个事务真的会同一时刻发生。如果没有隔离,就会出现三类典型的“厨房事故”:脏读、不可重复读、幻读。下文讲的四个隔离级别,就是针对这三类事故的不同防范等级。

明确了规约底线和事故清单,我们就能为四套“厨房使用规范”打分:哪套规范能防住哪些事故,一目了然。

ACID={A⏟原子性,C⏟一致性,I⏟隔离性,D⏟持久性},并发事故∈{脏读,不可重复读,幻读}\text{ACID} = \{ \underbrace{A}_{\text{原子性}}, \underbrace{C}_{\text{一致性}}, \underbrace{I}_{\text{隔离性}}, \underbrace{D}_{\text{持久性}} \}, \quad \text{并发事故} \in \{ \text{脏读}, \text{不可重复读}, \text{幻读} \}
这个公式说明:ACID是事务的四大属性,而我们要防的并发事故一共有三类,隔离级别就是针对它们的“防火等级”。
  1. 字母含义A、C、I、D四个字母分别代表事务的四大基本属性。
    A,C,I,DA, C, I, D
  2. 隔离性定位I是本文主角,它专门负责管理并发事务之间的互相干扰。
    I=隔离性I = \text{隔离性}
  3. 事故清单事务打架有三种典型事故,后面的所有隔离级别都在和它们博弈。
    并发事故∈{脏读,不可重复读,幻读}\text{并发事故} \in \{ \text{脏读}, \text{不可重复读}, \text{幻读} \}

读未提交

若合租厨房采取“完全自由”模式,大家想怎么用就怎么用。小李正在炒菜,夹起一块肉尝了尝,觉得太咸,可能下一秒就重新做。可小王站得近,已经看到了那块肉的颜色和摆盘,把它当成了小李的招牌菜配方记在心里。更糟的是,小李最后觉得不好吃,把肉整个倒掉了,小王却还惦记着那块“从未存在于餐桌”的肉——他读到了一份凭空想象出来的数据。这就是脏读。

在SQL世界里,读未提交(READ UNCOMMITTED)是最低隔离级别。事务可以读取其他事务尚未提交的数据,如果那个事务随后回滚,本事务读到的就是彻底作废的脏数据。这个级别几乎不加任何锁或版本控制,并发性能最好,但数据一致性最差,现实中极少被采用,除非业务对数据正确性毫不在乎(比如纯日志采集、无脑统计大数)。

读未提交就像没有门锁的厨房,谁都能探头看你锅里的半成品——哪怕那盘菜可能下一秒就被倒掉。生产环境几乎见不到它的身影,但它作为“警示底线”,让我们清楚知道不设防的世界有多混乱。

LRU:允许 {脏读,不可重复读,幻读}L_{RU}: \text{允许}\,\{ \text{脏读}, \text{不可重复读}, \text{幻读} \}
该公式表示读未提交对三类并发事故完全“不设防”,它是所有隔离级别中防护最弱的一档。
  1. 级别符号字母L加下标RU,代表读未提交这个隔离级别。
    LRUL_{RU}
  2. 允许集合三种并发事故在这一级别下全部可能发生,数据库不做任何拦截。
    允许 {脏读,不可重复读,幻读}\text{允许}\,\{ \text{脏读}, \text{不可重复读}, \text{幻读} \}

读已提交

室友们痛定思痛,投票通过新规约:“厨房里发生的一切,只有端上桌的菜才算数。”小李在灶台上煎牛排,加了多少次黄油、尝了几回咸淡,只要他没把牛排端上桌,小王就绝不会把它当成一道成菜。这样,脏读被成功杜绝。但新问题随之而来:小王做咖喱时,第一勺尝咸淡用的是旧盐罐,第二勺时小李恰好换了新盐罐。同一锅咖喱,两次尝出两种咸度——这就是不可重复读。

读已提交(READ COMMITTED)保证事务只能读到已经提交的数据,每次读取都获取当前最新的已提交值。它有效避免了脏读,但两个独立的SELECT之间,如果其他事务提交了修改,本事务前后就会读到不同的值。正因它在正确性与并发度之间取得了良好平衡,Oracle、PostgreSQL等数据库都把它设为默认隔离级别。

读已提交像一家“上桌才算菜”的餐厅后厨:你不会看到别人没端上桌的食材,却没法保证自己锅里两次调味的味道永远相同。对大多数业务来说,这个级别已经足够日常使用。

LRC:避免 {脏读},允许 {不可重复读,幻读}L_{RC}: \text{避免}\,\{ \text{脏读} \}, \quad \text{允许}\,\{ \text{不可重复读}, \text{幻读} \}
公式说明读已提交只堵住了脏读这一种事故,对不可重复读和幻读仍然门户大开。
  1. 级别符号L加下标RC,代表读已提交隔离级别。
    LRCL_{RC}
  2. 避免集合脏读被彻底堵住,事务读到的都是已提交的“餐桌成品”。
    避免 {脏读}\text{避免}\,\{ \text{脏读} \}
  3. 允许集合同一事务内多次读取,仍可能因为他人提交而读到不同数值。
    允许 {不可重复读,幻读}\text{允许}\,\{ \text{不可重复读}, \text{幻读} \}

可重复读

室友们又被不可重复读折磨得不轻,于是引入更聪明的制度:每个厨师进厨房的瞬间,管理员给他拍一张“厨房全景照片”作为快照。小王拿着照片,不管别人往冰箱里塞了什么、换了哪罐盐,他做菜时只认照片里的样子。两次尝盐,一定尝到同一罐盐——不可重复读被根治了。但如果小张在照片之外偷偷往菜单上加了一道“隐藏菜”,小王按照片对菜单,从头到尾都不知道这道菜的存在,直到做完饭对账才惊讶地发现:咦,怎么凭空多出一道菜?这就是幻读。

可重复读(REPEATABLE READ)是MySQL InnoDB的默认隔离级别。它通过MVCC在事务开始时生成一致性快照,事务内所有普通SELECT都基于该快照读取,因此前后读必然一致。但按SQL标准,其他事务新插入的行不会被本事务快照涵盖,因此幻读仍然可能发生。需要说明的是,MySQL InnoDB在可重复读下通过间隙锁(next-key lock)额外压制了大部分幻读,这是它对标准的一种增强——但这属于数据库实现层面的“加餐”,并非标准默认承诺。

可重复读就像给每个厨师发了一张入场快照:照片里的世界永远稳定,只是照片外的世界仍在悄悄生长。它非常适合对报表一致性有要求的读多写少场景。

LRR:避免 {脏读,不可重复读},可能 {幻读}L_{RR}: \text{避免}\,\{ \text{脏读}, \text{不可重复读} \}, \quad \text{可能}\,\{ \text{幻读} \}
公式表明可重复读用快照挡住了前两种事故,但标准定义下“新插入的行数变化”仍可能漏进来。
  1. 级别符号L加下标RR,代表可重复读隔离级别。
    LRRL_{RR}
  2. 避免集合脏读和不可重复读都被快照机制挡在门外。
    避免 {脏读,不可重复读}\text{避免}\,\{ \text{脏读}, \text{不可重复读} \}
  3. 可能异常快照之外的新增行不会进入视野,导致幻读。
    可能 {幻读}\text{可能}\,\{ \text{幻读} \}

串行化

最极端的规约来了:厨房门口挂上一把大铁锁,一次只放一个人进去做饭。小王用完了、菜端出来了、厨房收拾干净了,下一个小李才能进去。这样的世界绝对不会有任何干扰——你看到的、用到的、做成的一切,都是你独占厨房的成果。代价也惨痛:晚饭高峰期六个人排队,等最后一个人吃上饭,已经深夜了。

串行化(SERIALIZABLE)是最高隔离级别。事务逐个执行,最终效果等价于串行调度,彻底避免脏读、不可重复读、幻读。数据库通过加锁(表锁、区间锁)或冲突检测实现。它的一致性最强,但并发性能断崖式下降:锁等待、阻塞、超时频繁,吞吐量远低于其他级别。只有在正确性要求极高且并发量不大的场景——比如资金结算、账务对账、库存清点——才会选用。

串行化是“一个厨房一把锁”的终极方案:安全满分,效率垫底。它提醒我们,隔离级别不是越高越好,而是刚刚好才好——用最合适的成本换取业务可接受的一致性。

LS:避免 {脏读,不可重复读,幻读}L_S: \text{避免}\,\{ \text{脏读}, \text{不可重复读}, \text{幻读} \}
公式说明串行化把三类并发事故全部禁止,以牺牲并发换取的绝对正确。
  1. 级别符号L加下标S,代表串行化隔离级别。
    LSL_S
  2. 全面防护三类事故一条不漏全部禁止,事务之间完全互斥。
    避免 {脏读,不可重复读,幻读}\text{避免}\,\{ \text{脏读}, \text{不可重复读}, \text{幻读} \}

MVCC原理

可重复读的“入场快照”究竟是怎么实现的?总不能让数据库真的给每个厨师打印一张实体照片吧!MVCC的答案是:数据库为每行数据保存多个版本,就像冰箱里同一食材的多个“进货批次”。每个事务被分配一个专属的“食材视图”,你看到的不是冰箱的最新状态,而是进入厨房那一刻的食材批次。每份食材都贴着标签:是哪个事务放进来的(trx_id),它之前的版本在哪里(roll_pointer)。

MVCC(Multi-Version Concurrency Control,多版本并发控制)是现代数据库的主流实现。每个数据行都有隐藏字段:DB_TRX_ID记录最近修改该行的事务ID,DB_ROLL_PTR指向undo log中旧版本的回滚指针。旧版本通过undo log串成一条版本链。事务读取时,沿版本链找到对当前事务可见的版本。可见性规则的核心是:版本必须由已提交且早于快照创建的事务产生,或者干脆就是当前事务自己写的。这样,读操作不加锁,写操作也不阻塞普通读,读写在MVCC下可以并行,极大提升并发度。

MVCC就是“多批次食材+专属视图”的数据库工程化实现。它让快照读与写入并行不悖,是读已提交和可重复读高效落地的幕后功臣,也是高并发系统能跑得又快又稳的秘密武器。

可见(v)={trueif trx_id(v) 已提交且 trx_id(v)<snapshot_idtrueif trx_id(v)=my_trx_idfalseotherwise\text{可见}(v) = \begin{cases}\mathrm{true} & \text{if } \mathrm{trx\_id}(v) \text{ 已提交且 } \mathrm{trx\_id}(v) < \mathrm{snapshot\_id} \\\mathrm{true} & \text{if } \mathrm{trx\_id}(v) = \mathrm{my\_trx\_id} \\\mathrm{false} & \text{otherwise}\end{cases}
这个公式是MVCC快照读的判决书:满足“老版本已提交”或“自己写的版本”两个条件之一就可见,否则一律视而不见。
  1. 候选版本v代表版本链上某一个历史版本,每次读都需要逐个判断。
    v∈版本链v \in \text{版本链}
  2. 条件一该版本由更早的已提交事务写入,早于快照创建时间,因此对当前事务可见。
    trx_id(v) 已提交且 trx_id(v)<snapshot_id\mathrm{trx\_id}(v) \text{ 已提交且 } \mathrm{trx\_id}(v) < \mathrm{snapshot\_id}
  3. 条件二版本来自当前事务自身,自己写的数据当然对自己可见。
    trx_id(v)=my_trx_id\mathrm{trx\_id}(v) = \mathrm{my\_trx\_id}
  4. 其余不可见版本来自尚未提交或更晚的事务,直接跳过寻找上一版。
    false\mathrm{false}

级别选择

四套厨房规约没有绝对优劣,只看你们的公寓是什么氛围。全是自己人、做饭随意尝——读未提交;有客人来访、至少要讲究“上桌才算菜”——读已提交;你们要开一家对外营业的私房菜馆、每道菜的口味必须前后一致——可重复读;做的若是手术室级别的营养配餐、一丝差错都不能有——串行化。规矩越严,心里越踏实,但大家做饭的速度也越慢。

实际工程中,隔离级别选择本质是正确性与性能的权衡。级别越高,锁越多,性能越差。主流数据库默认值各异:MySQL InnoDB默认REPEATABLE READ,配合MVCC和间隙锁,实际防护已接近串行化;PostgreSQL、Oracle、SQL Server默认READ COMMITTED。报表分析、订单查询等读多写少场景,可重复读的快照保证报表前后口径一致;高并发写入的核心交易链路,读已提交往往能在不损失必要一致性的同时给出最佳吞吐;资金对账等强一致场景才考虑串行化。

选隔离级别,本质是给公寓选“厨房规约”:既要吃得放心,又不能让室友们饿肚子。经验丰富的DBA会在正确性红线与业务吞吐之间,找到那条最舒适的平衡线。

隔离级别↑⟹一致性↑, 并发性能↓\text{隔离级别} \uparrow \quad \Longrightarrow \quad \text{一致性} \uparrow,\ \text{并发性能} \downarrow
公式展示了隔离级别选择的根本规律:级别越高,一致性越强,但并发吞吐越低,两者不可兼得。
  1. 级别上升从读未提交向串行化逐级递进。
    隔离级别↑\text{隔离级别} \uparrow
  2. 一致性上升被防住的并发事故越来越多,数据越来越“干净”。
    一致性↑\text{一致性} \uparrow
  3. 性能下行锁等待与版本管理开销同步增大,吞吐量明显回落。
    并发性能↓\text{并发性能} \downarrow

实战排查

作为公寓管理员,你需要随时知道:现在执行的是哪套规约?哪间房的厨师在违规操作?出了问题去哪查记录?SQL世界也提供了一整套“管理工具”:查看当前的隔离级别、为会话切换隔离级别、诊断锁等待与死锁。

查看隔离级别,MySQL 8.0用SELECT @@transaction_isolation;,5.7用SELECT @@tx_isolation;;PostgreSQL用SHOW default_transaction_isolation;。设置隔离级别,执行SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;只影响当前会话,使用GLOBAL关键字则影响全局新连接。排查并发问题,SHOW ENGINE INNODB STATUS\G能查看锁与死锁信息,information_schema.innodb_trx能列出当前活跃事务及其持锁情况。

掌握了这套“管理员工具”,你就能在线上事故发生时快速判断:是哪个环节的规约出了漏洞,是脏读、不可重复读还是幻读,从而对症下药,让数据库恢复平稳运行。

设置语句:SET TRANSACTION ISOLATION LEVEL ∈{READ UNCOMMITTED,READ COMMITTED,REPEATABLE READ,SERIALIZABLE}\text{设置语句}: \texttt{SET TRANSACTION ISOLATION LEVEL } \in \{ \text{READ UNCOMMITTED}, \text{READ COMMITTED}, \text{REPEATABLE READ}, \text{SERIALIZABLE} \}
公式给出了标准SQL切换隔离级别的语法框架,四个可选值分别对应前面讲过的四套“厨房规约”。
  1. SET语句数据库提供的修改隔离级别的标准SQL命令。
    SET TRANSACTION ISOLATION LEVEL\texttt{SET TRANSACTION ISOLATION LEVEL}
  2. 四个取值从读未提交到串行化,四选一填入即可生效。
    READ UNCOMMITTED,READ COMMITTED,REPEATABLE READ,SERIALIZABLE\text{READ UNCOMMITTED}, \text{READ COMMITTED}, \text{REPEATABLE READ}, \text{SERIALIZABLE}

AI 应用场景

电商秒杀防超卖

阶段:业务数据一致性保障(SQL事务设计与隔离级别选型)

解决的问题:秒杀场景下,成千上万用户同时抢购同一件商品。传统不加控制的读写在“检查库存→扣减库存”之间容易被其他事务穿插,导致超卖。AI推荐系统触发的瞬时流量洪峰更放大了这个风险。利用可重复读配合行锁,把库存扣减写成UPDATE stock SET num = num - 1 WHERE num > 0这种当前读,写操作之间天然串行,就能守住库存底线。

具体案例:某电商平台晚8点秒杀,活动商品库存只有500件。后端服务用MySQL InnoDB默认的REPEATABLE READ,商品行作为热点行,所有扣减请求通过行锁排队执行。最终实际成交恰好500单,无一超卖,同时普通查询走MVCC快照,页面浏览毫秒级响应。

银行夜间批量对账

阶段:数据抽取与报表生成(隔离级别选择与MVCC快照)

解决的问题:夜间批处理任务要扫描全量用户余额生成对账单。若用读已提交,批处理期间用户仍在转账,同一账户两次读到不同余额,报表口径完全混乱。切换到可重复读,批处理事务启动时生成一致性快照,整个扫描过程读到的都是同一时点的余额,与线上交易互不干扰。

具体案例:某银行核心系统每天凌晨2点跑批,业务高峰期白天的流水达到千万级。DBA将对账批处理会话设为REPEATABLE READ,快照隔离保证对账单与交易流水完全对应,同时线上联机交易继续走READ COMMITTED保持高吞吐,两类任务井水不犯河水。

数据仓库增量抽取

阶段:数据集成与管道构建(可重复读快照保证抽取一致性)

解决的问题:数据同步任务在抽取一张大表时,源表仍在被持续写入。若每次SELECT都读最新值,抽取结果会出现“同一主键前后字段来自不同版本”的数据漂移。使用可重复读隔离级别的会话开启快照,一次性抽取出事务开始时刻的一致数据视图。

具体案例:某数据团队用SQL抽取订单表同步到数仓,抽取耗时3分钟。把抽数会话设为REPEATABLE READ后,3分钟内源表新产生的订单不会混入本次抽取,下游ETL不再出现主键冲突和字段错位,数据质量显著提升。

Python 演示

python53 行
class MiniMVCC:    """极简MVCC模拟器: 演示四种隔离级别下事务读到的数值差异"""     def __init__(self, initial=100):        self.base_value = initial        # 已提交的最新值        self.buffer_map = {}             # 未提交写入缓存        self.snapshot_map = {}           # 各事务的快照        self.version_list = []           # 已提交版本链        self._trx_counter = 0     def begin(self):        self._trx_counter += 1        return self._trx_counter     def write(self, trx_id, new_value):        self.buffer_map[trx_id] = new_value     def commit(self, trx_id):        value = self.buffer_map.pop(trx_id)        self.version_list.append((trx_id, value))        self.base_value = value     def rollback(self, trx_id):        self.buffer_map.pop(trx_id, None)     def read(self, trx_id, level):        pending = list(self.buffer_map.values())        if level == "读未提交":            return pending[-1] if pending else self.base_value        if level == "读已提交":            return self.base_value        if level == "可重复读":            return self.snapshot_of(trx_id)        return self.base_value       # 串行化: 等锁后读最新     def snapshot_of(self, trx_id):        if trx_id not in self.snapshot_map:            self.snapshot_map[trx_id] = self.base_value        return self.snapshot_map[trx_id]  if __name__ == "__main__":    db = MiniMVCC(initial=100)    t1 = db.begin()    t2 = db.begin()    db.write(t1, 500)                    # T1 将余额改为 500, 未提交    print("T1 未提交时 T2 读到的余额:")    for level in ["读未提交", "读已提交", "可重复读", "串行化"]:        print(f"  [{level}] -> {db.read(t2, level)}")    db.commit(t1)                        # T1 提交    print("T1 提交后 T2 读到的余额:")    for level in ["读未提交", "读已提交", "可重复读", "串行化"]:        print(f"  [{level}] -> {db.read(t2, level)}")
目的:用约50行Python模拟MVCC多版本并发控制,直观对比四种隔离级别下事务T2读到的数值差异。
  1. self.buffer_map = {}未提交写入缓存,对应“厨房里没端上桌的半成品”,脏读的源头。
  2. self.snapshot_map = {}各事务的快照字典,可重复读的“入场照片”就存在这里。
  3. def write(self, trx_id, new_value)把新值写入未提交缓存,模拟事务修改数据但还没提交。
  4. def commit(self, trx_id)从缓存取出值写入版本链,已提交值更新,模拟“菜端上桌”。
  5. if level == "读未提交"直接返回未提交缓存里的最新值,完整演示脏读路径。
  6. return self.snapshot_of(trx_id)可重复读永远走快照,事务内读取结果前后一致。

常见误区

误区学完四个级别后,我认为可重复读既然“读到的都一样”,那就不可能再看到任何新数据,幻读一定不存在。

正解在SQL标准定义下,可重复读明确允许幻读:快照能挡住已存在行的变更,但挡不住其他事务插入新行。MySQL InnoDB的RR因为额外加了间隙锁才几乎消灭幻读,这是厂商增强而非标准默认。PostgreSQL的RR下,如果两个查询条件不同,仍可能看到新插入的行。判断级别能力要“先标准后实现”。

为何常见:很多人日常只用MySQL,把MySQL的实现行为当成了SQL标准的通用定义,忽略了标准与实现的差别。

误区串行化最安全,那生产环境干脆全部用串行化,一劳永逸不是更好吗?

正解串行化靠锁把并发变成排队,吞吐量骤降,锁等待和超时概率激增。一致性只是数据库追求的目标之一,可用性与性能同样重要。正确做法是分类对待:核心账务才用串行化,普通交易用读已提交,报表查询用可重复读,让每一类业务拿到恰到好处的隔离强度。

为何常见:初学者只盯着正确性看,忽略了数据库在高并发下“快”也是硬指标,没有形成权衡思维。

误区MVCC宣传“读写互不阻塞”,所以数据库在MVCC下根本不需要锁,所有并发问题都靠多版本解决。

正解MVCC让普通快照读不加锁、不阻塞写,但写与写之间、当前读(UPDATE、DELETE、SELECT …… FOR UPDATE)仍然必须加锁。MVCC解放的是“读写并发”,不是“写写并发”的免死金牌。两个事务同时改同一行,照样需要一个排队等待另一个。

为何常见:把“读写不互相阻塞”的营销话术扩大化理解,忽略了写写冲突这个最朴素的并发矛盾。

误区听名字以为脏读就是读到没来得及更新的旧数据,就像菜放久了不新鲜。

正解恰恰相反,脏读读到的是“太新”的数据——别人还没提交、随时可能回滚消失的数据。比如看到余额5000是别人转账未提交的结果,对方回滚后真实余额其实是0,你基于5000做的任何决策都建立在沙滩上。旧数据不可怕,假数据才致命。

为何常见:中文“脏”字让人联想到“脏乱、陈旧、过期”,但这里的“脏”指的是“未敲定、可能作废”,与新旧无关。

面试题

Q1. 在哪个隔离级别下,事务可以读取到其他事务尚未提交的数据?

单选题 · easy

  • A.读未提交(READ UNCOMMITTED)← 正确答案
  • B.读已提交(READ COMMITTED)
  • C.可重复读(REPEATABLE READ)
  • D.串行化(SERIALIZABLE)

答案:A。读未提交是唯一允许读取未提交数据的级别,其余三个级别都至少要求读到“已提交”的数据。就像只有“完全自由”的厨房才允许你窥探别人锅里的半成品。

Q2. 以下哪种现象描述的是“同一事务内两次查询返回不同结果”?

单选题 · easy

  • A.脏读,读到未提交数据
  • B.不可重复读,读同一行值变化← 正确答案
  • C.幻读,查询行数变多
  • D.死锁,互相持锁等待

答案:B。不可重复读的定义就是同一事务内两次读同一行,因其他事务提交修改而结果不同。脏读是读到未提交数据,幻读是结果集行数变化,死锁是锁资源循环等待,三者都不是“同一行值变化”的核心特征。

Q3. MVCC 中“多版本”的旧版本数据主要存放在哪里?

单选题 · easy

  • A.回滚段中的 undo 日志← 正确答案
  • B.主键索引的叶子节点
  • C.客户端本地缓存区
  • D.服务端全局缓存池

答案:A。每行数据的旧版本通过隐藏字段 DB_ROLL_PTR 指向 undo log 中的记录,旧版本串成的版本链就存放在回滚段里。它好比厨房管理员保存的一摞“食材历史进货单”,要吃旧批次随时可查。

Q4. MySQL InnoDB 默认的隔离级别是哪个?

单选题 · mid

  • A.READ UNCOMMITTED(读未提交)
  • B.READ COMMITTED(读已提交)
  • C.REPEATABLE READ(可重复读)← 正确答案
  • D.SERIALIZABLE(串行化)

答案:C。MySQL InnoDB 默认是 REPEATABLE READ,配合 MVCC 快照和间隙锁,实际防护能力接近串行化。Oracle、PostgreSQL 默认则是 READ COMMITTED,各数据库默认值需要分开记。

Q5. 按 SQL 标准,哪个隔离级别能够保证完全避免幻读?

单选题 · mid

  • A.读未提交,最宽松级别
  • B.读已提交,只读已提交数据
  • C.可重复读,快照保证一致
  • D.串行化,事务完全排队← 正确答案

答案:D。标准定义下,只有串行化通过完全互斥彻底避免幻读。可重复读只能保证已存在行的值不变,拦不住新插入的行;MySQL 的 RR 靠间隙锁增强了防幻读能力,但那是实现层面的加分项。

Q6. 在 MySQL 可重复读下,事务的普通 SELECT 基于什么读取数据?

单选题 · mid

  • A.每次读取数据库当前最新已提交值
  • B.事务开始时生成的一致性快照← 正确答案
  • C.数据库全局唯一的缓存副本
  • D.最新未提交的裸数据版本

答案:B。可重复读下普通 SELECT 是快照读,事务第一次读时生成一致性快照,后续读都基于它。这就像厨师进厨房时拍了一张全景照片,之后只认照片里的样子,不受他人后续操作影响。

Q7. 读已提交下,事务内两次执行相同 SELECT 读到不同值,这属于?

单选题 · hard

  • A.脏读,说明隔离级别失效
  • B.不可重复读,RC 下的正常现象← 正确答案
  • C.幻读,需要升级隔离级别
  • D.死锁,引擎自动回滚处理

答案:B。读已提交每次读都取最新已提交值,因此两次读之间若有其他事务提交,结果自然不同,这正是不可重复读。它是 RC 设计上允许的现象,不是故障。想避免它,需要升级到可重复读。

Q8. 读已提交隔离级别允许出现以下哪些并发现象?

多选题 · mid

  • A.脏读,未提交数据被读到
  • B.不可重复读,前后读数不同← 正确答案
  • C.幻读,结果集行数变化← 正确答案
  • D.丢失更新,写覆盖他人

答案:BC。读已提交避免了脏读,但允许不可重复读和幻读。丢失更新不属于SQL标准定义的三大并发异常集合,且在不同隔离级别和具体实现下表现复杂,不能简单归入RC的“允许清单”。

Q9. 关于 MVCC 的可见性判断,下列说法正确的是?

多选题 · hard

  • A.版本早于快照且已提交则可见← 正确答案
  • B.当前事务自己写入的版本可见← 正确答案
  • C.未提交版本对他人同样可见
  • D.版本链支撑快照读取与回滚← 正确答案

答案:ABD。可见性规则是“已提交且事务ID早于快照”或“自己写的版本”。未提交版本的写入对其他事务不可见,否则就成了脏读。版本链既支持快照读取历史版本,也支持事务回滚时恢复旧值,C选项正好说反了。

Q10. 从读未提交提升到串行化,可能带来哪些代价?

多选题 · mid

  • A.并发吞吐量显著下降← 正确答案
  • B.锁等待与死锁概率上升← 正确答案
  • C.所有数据绝对安全
  • D.事务响应时间变长← 正确答案

答案:ABD。级别越高锁越重,事务排队越多,吞吐量下降、等待与死锁概率上升、响应变长,这些都是代价。C选项“数据绝对安全”是收益而非代价,且“绝对”二字过于绝对,不能选。