想象你是一座繁忙图书馆的管理员。过去要查资料,整个馆得清场让位——读者等管理员,管理员等读者,谁也没法同时干活。后来馆里配了一台“全景快照相机”:每位读者进门时,机器咔嚓一下拍下当前所有书架的照片。读者之后坐在阅览室,只看照片翻查,管理员照常上书改架,互不干扰。这个聪明的设计,就是数据库里的快照隔离。本篇用图书馆故事串场,讲清事务如何拍快照、如何沿版本链找到对的书、为何两个管理员偶尔会把书架改坏(写偏斜),以及它和串行化的界限。学完你会明白,并发控制不是一味“排队”,而是用一份聪明的“照片”换取更高的吞吐量。

岗位关联

快照隔离是后端工程师理解事务隔离级别、排查线上读写阻塞与并发异常的必备武器,掌握它等于掌握了数据库并发控制的半壁江山。

目录
  1. 可视化演示
  2. 核心概念
  3. MVCC原理
  4. 快照建立
  5. 读写并发
  6. 异常现象
  7. 隔离级别
  8. 实现技术
  9. 应用实践
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示数据库快照隔离的核心机制,通过源头/管道/节点/终端四种模式展示关键概念与交互流程。

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

数据库快照隔离:快照隔离 快照隔离的核心思想与读写不阻塞 MVCC多版本并发控制机制 核心机制可视化 数据库快照隔离整体架构与关键组件

核心概念

图书馆的“全景快照”拍的是什么?不是照片本身,而是规则:每位读者进门那一刻,馆内的书是什么状态、编号排到第几号、哪些人还在场内。数据库的快照隔离同样如此——它把“拍照片”变成一种事务级的视图,而不是真正复制一遍数据。

快照隔离(Snapshot Isolation,SI)是数据库并发控制的一种策略。它的核心承诺是:一个事务在快照建立之后的所有读操作,都只看到那一刻已提交的数据状态,就像读者全程只看那张照片。其他事务在这期间提交的数据,对当前事务不可见,直到当前事务结束。

快照隔离的底层支撑是MVCC。数据行不只有当前值,还保留历史版本;事务通过“版本可见性规则”判断哪个版本属于自己。可见性规则可简记为:版本必须已提交、版本创建事务号要小于当前事务号、不能是快照时活跃事务写的。前两点保证“照片只含已定稿的书”,第三点保证“照片不带未离场的人”。

所以,快照隔离不是“所有数据都复制一份”,而是给每个事务发一张带规则的“入场照”。照片里能看到谁、不能看到谁,全由这条可见性公式说了算。理解它,就握住了快照隔离的钥匙。

Visible(v,S)=(v.txid<S.txid)∧(v.txid∉S.active)\text{Visible}(v, S) = (v.\text{txid} < S.\text{txid}) \land (v.\text{txid} \notin S.\text{active})
快照可见性规则:一个版本只有在“创建编号更小”且“创建者不在活跃集合里”时,才会出现在快照中。
  1. 版本事务号v.txid 表示哪个事务生成了这个版本,相当于修订本上的印戳编号。
    v.txidv.\text{txid}
  2. 当前事务号S.txid 表示当前读事务的编号,即读者进馆时拿到的入场凭证号。
    S.txidS.\text{txid}
  3. 编号比较v.txid < S.txid 确保版本诞生于读事务之前,照片里才可能出现它。
    v.txid<S.txidv.\text{txid} < S.\text{txid}
  4. 活跃集合排除S.active 记录拍照瞬间还在执行的事务;若版本作者在其中,说明照片拍下时这本书还没定稿,必须从照片中剔除。
    v.txid∉S.activev.\text{txid} \notin S.\text{active}

MVCC原理

图书馆的书架暗格里存着每本书的历次修订本:第一版、第二版、第三版……读者拿的是旧照片,就翻旧修订本;管理员放的是新修订本,旧本也不撤走。这种“新旧并存”的存放方式,就是MVCC的日常样貌。

MVCC(Multi-Version Concurrency Control,多版本并发控制)把数据库的每一行都想象成一摞“修订本”。每次事务更新某行时,不直接覆盖,而是追加一个新版本,旧版本继续保留。因为旧版本还在,读旧快照的事务才能找到它需要的数据。

所有版本通过指针连成版本链(version chain),链头是最新版本。要找一个旧版本,就从链头沿 prev 指针往回走,直到找到对当前快照可见的那个版本。事务ID(txid)是全局递增的编号,用来给每个版本盖章并决定可见性。版本可见性判断是MVCC的核心,上一节那条可见性公式正是它的数学表达。

版本链就像书的“修订沿革页”:谁在什么时候改过、改成什么样,一页页翻过去就一清二楚。MVCC用这条链换取了宝贵的并发度——新读者看新本,老读者翻旧本,大家各看各的版本,谁也不挡谁的路。

Chain(r)=⟨vk,vk−1,…,v1⟩,vi.prev=vi−1\text{Chain}(r) = \langle v_k, v_{k-1}, \dots, v_1 \rangle,\quad v_{i}.\text{prev}=v_{i-1}
一行数据的版本链:最新版本在链头,旧版本通过 prev 指针一路连接,读旧数据就是沿着链向后找。
  1. 链式结构Chain(r) 表示数据行 r 的全部历史版本按时间倒序排列。
    Chain(r)\text{Chain}(r)
  2. 链头版本v_k 是最新版本,读者翻书时最先摸到的就是它。
    ⟨vk,…,v1⟩\langle v_k, \ldots, v_1 \rangle
  3. 回溯指针v_i.prev = v_{i-1} 说明每个旧版本都指向更新的一个版本,形成可以倒着走的链表。
    vi.prev=vi−1v_{i}.\text{prev}=v_{i-1}

快照建立

读者不是一进门就拿到照片——他得先走到门口,管理员咔嚓一下按下快门,记录“现在馆内活跃的人”和“最新入场编号”。数据库也挑特定时刻建立快照,而不是随时都拍。

快照的建立时机有两种常见策略:事务第一条语句执行时建立(读时快照),或事务BEGIN时就建立(开始时快照)。PostgreSQL默认在事务执行第一条语句时创建快照,避免纯粹的“空转事务”浪费资源。无论是哪种,一旦建立,整个事务期间都不再变化。

快照不是复制所有数据,而是一个轻量结构:记录当前最大的已分配事务号(maxTxid),以及此刻仍在执行的事务集合(ActiveSet)。判断可见性时,把版本的创建事务号与这个集合比较即可。由于只保存两个小集合,快照建立的成本极低,高频事务也能承受。

拍快照的成本低得惊人:管理员只是瞄了一眼号码牌和在场名单,并没有把整个图书馆复印一遍。这份“轻量照片”让快照隔离在高并发下依然能频繁建立,是它比纯复制方案实用的关键原因。

Ssnap=⟨maxTxid,ActiveSet⟩,ActiveSet={t∣t active at snapshot}S_{\text{snap}} = \langle \text{maxTxid}, \text{ActiveSet} \rangle,\quad \text{ActiveSet} = \{ t \mid t \text{ active at snapshot} \}
快照结构:一张照片的底片上只有两行字——“最新入场编号”和“此刻还在馆内的人”。
  1. 快照组成快照结构只有两部分,像照片底片上写着的两行字:最大编号和在场人员。
    ⟨maxTxid,ActiveSet⟩\langle \text{maxTxid}, \text{ActiveSet} \rangle
  2. 最大事务号maxTxid 是拍照瞬间最新发出的编号,晚于它的版本都属于“未来”。
    maxTxid\text{maxTxid}
  3. 活跃事务集合ActiveSet 收集当时还没结束的事务,这些事务的书稿在照片里一律不算数。
    {t∣t active at snapshot}\{ t \mid t \text{ active at snapshot} \}

读写并发

有了照片,读者不再需要管理员“别动、等我读完”的配合。管理员上书时,读者正低头翻照片;读者找书时,管理员也照常整架——互不阻塞,收益立竿见影。

快照隔离带来的最直观好处是读写并行。读事务不需要获取共享锁,因为要读的数据都在快照里;写事务也不需要等读事务结束,因为写入时直接生成新版本,不覆盖旧版。两者在同一时刻操作同一行也互不等待,这是快照隔离被广泛采用的最大卖点。

注意:快照隔离并不同时容忍两个写者修改同一行。当两个写事务先后修改同一行时,先提交者“获胜”,后提交者会检测到版本已变化,事务被中止并要求重试。这一规则叫“首次更新获胜”。写写冲突虽然存在,但在读多写少的系统中,冲突概率低,整体并发能力大幅提升。

读者全在闷头看照片,管理员悄悄上书,同一时间谁也不用歇着。唯一的代价是“两个管理员同时改同一本书”时,馆里要请其中一位重来一次——这提醒我们,快照隔离把读写并发做到了极致,但写写冲突从未消失。

TSI=max⁡(Tr,Tw),T2PL=Tr+TwT_{\text{SI}} = \max(T_r, T_w), \qquad T_{\text{2PL}} = T_r + T_w
并发度对比:快照隔离下读写并行,总耗时约等于较慢的一方;两阶段锁下读写串行,总耗时是两者相加。
  1. SI总耗时T_SI 是快照隔离下完成一个读事务加一个写事务的总时长。
    TSI=max⁡(Tr,Tw)T_{\text{SI}} = \max(T_r, T_w)
  2. 二阶段锁总耗时T_2PL 是两阶段锁(读写互斥)下的总时长。
    T2PL=Tr+TwT_{\text{2PL}} = T_r + T_w
  3. 收益条件当读时间远大于写时间时,max 远小于求和,这就是快照隔离的加速来源。
    Tr≫Tw⇒TSI≪T2PLT_r \gg T_w \Rightarrow T_{\text{SI}} \ll T_{\text{2PL}}

异常现象

照片也有闯祸的时候。图书馆规定:红色书和蓝色书的重量之和不得超过100斤。管理员A看着照片,把红书换成更重的;管理员B也看着同一张照片,把蓝书换成更重的。各自看来都没超重,合起来却超了——这,就是写偏斜。

写偏斜(Write Skew)是快照隔离最著名的异常。两个事务各自修改不同的数据行,写入集不相交,因此不会触发写写冲突;但它们都基于同一份旧快照做判断,而这些判断依赖的数据在对方提交后发生了变化。最终,一个数据库约束可能被破坏。

更形式化地说,写偏斜需要三个条件同时成立:写入集互不相交(所以常规冲突检测发现不了);读取集有交集(两个事务看到同一份公共状态);提交后某个业务约束被打破。快照隔离能防脏读、防不可重复读、防丢失更新,却防不住写偏斜,这是它无法等价于串行化的关键论据。

两个管理员都觉得自己没违规,照片却让他们一起踩了红线。写偏斜的教训是:并发控制不能只盯着“谁改了同一行”,还要警惕“谁基于同一份旧认知做了决策”。涉及公共约束的更新,需要上升到串行化隔离级别去保护。

W=(WriteSet(T1)∩WriteSet(T2)=∅)∧(ReadSet(T1)∩ReadSet(T2)≠∅)∧¬C(after commit)W = \big( \text{WriteSet}(T_1) \cap \text{WriteSet}(T_2) = \varnothing \big) \land \big( \text{ReadSet}(T_1) \cap \text{ReadSet}(T_2) \neq \varnothing \big) \land \neg C(\text{after commit})
写偏斜条件:写入集合不相交 + 读取集合相交 + 提交后约束失效,三个条件缺一不可。
  1. 写集不相交两个事务操作不同的行,常规的写写冲突检测因此“失灵”。
    WriteSet(T1)∩WriteSet(T2)=∅\text{WriteSet}(T_1) \cap \text{WriteSet}(T_2) = \varnothing
  2. 读集相交两个事务读过共同的旧数据,判断依据是同一份快照。
    ReadSet(T1)∩ReadSet(T2)≠∅\text{ReadSet}(T_1) \cap \text{ReadSet}(T_2) \neq \varnothing
  3. 约束被破坏提交后,数据库的完整性约束 C 不再成立。
    ¬C(after commit)\neg C(\text{after commit})

隔离级别

图书馆可以按“严格程度”设置不同规则:完全不拍照(读到什么算什么)、每次翻书拍一张(保证每次阅读新鲜)、进门拍一张(全程一张照片)、甚至干脆一次只放一位读者入馆(绝对排队)。这些规则,对应着不同的事务隔离级别。

数据库事务隔离级别从弱到强分为:读未提交(RU)、读已提交(RC)、可重复读(RR)、串行化(SERIALIZABLE)。快照隔离是MVCC数据库对“可重复读”或更高语义的一种经典实现:它以整个事务为周期创建一致性快照,因此事务内的多次读结果一致。

与读已提交相比,快照隔离不会出现“同一事务两次读结果不同”的不可重复读;与串行化相比,快照隔离不能防止写偏斜。在PostgreSQL中,READ COMMITTED每条语句取新快照,REPEATABLE READ和SERIALIZABLE都用快照隔离起步,但SERIALIZABLE在提交阶段增加了冲突检测来阻止写偏斜。理解这些级别的位置,才能为业务选择正确的隔离保证。

选隔离级别,本质上是在选“照片的保质期”:保质期太短,读不稳定;保质期太长,又可能让管理员拿着旧照片做出错误决策。快照隔离站在中间位置,用一张贯穿全程的照片换来了高并发,也把写偏斜的担子递给了开发者。

RU≺RC≺SI/RR≺SS\text{RU} \prec \text{RC} \prec \text{SI/RR} \prec \text{SS}
隔离强度排序:从不拍照片的读未提交,到每次语句拍照的读已提交,再到事务级快照的快照隔离,最后到全馆排队的串行化。
  1. 读未提交RU 级别完全不拍快照,可能读到管理员还没登记完的“半本”书。
    RU\text{RU}
  2. 读已提交RC 级别每条语句拍一张照,能防脏读,但两次读之间照片会换。
    RC\text{RC}
  3. 快照隔离SI/RR 级别整个事务只拍一张照,读稳定,但仍可能写偏斜。
    SI/RR\text{SI/RR}
  4. 串行化SS 级别让事务像排队进馆一样串行执行,从根上消除写偏斜等异常。
    SS\text{SS}

实现技术

照片上的每个条目除了书名,还有一个不起眼的“修订编号”小字;馆内还会定期清理完全没人再用的旧修订本。这两个细节,就是数据库行头部字段与垃圾回收的故事。

在PostgreSQL等MVCC数据库里,每行数据的元组头部都藏着两个关键字段:xmin(创建该版本的事务号)和xmax(删除或覆盖该版本的事务号)。正是靠这两个字段,版本可见性判断才能在没有全局锁的情况下完成。

一个版本对某事务可见,需要满足xmin早于该事务的快照、xmax晚于该事务的快照(或无删除)等条件。旧版本不会立刻物理删除,因为可能有老快照还在使用;垃圾回收器(VACUUM)只在没有任何活跃事务的可见范围包含某个旧版本时才清理它。索引通常不存储可见性信息,而是先定位索引项,再回表检查元组头部。

修订编号让小字决定了谁看得见这本书,垃圾回收则确保旧修订本不会永远占着书架。快照隔离的实现,不是靠魔法,而是靠“一行小字 + 定期清仓”,把高并发和高可用同时扛了下来。

Reclaimable(v)⇔∄T∈Active(t):Visible(v,ST)\text{Reclaimable}(v) \Leftrightarrow \nexists T \in \text{Active}(t): \text{Visible}(v, S_T)
可回收判定:只要还存在某个活跃事务的快照能看到这个旧版本,它就不能被物理删除。
  1. 活跃事务集Active(t) 表示当前时刻所有还在执行的事务。
    Active(t)\text{Active}(t)
  2. 可见性扫描对每个活跃事务的快照逐一检查,看旧版本 v 是否可见。
    Visible(v,ST)\text{Visible}(v, S_T)
  3. 不存在才回收没有任何活跃事务能看到 v 时,v 才被标记为可回收。
    ∄T∈Active(t)\nexists T \in \text{Active}(t)

应用实践

图书馆馆长决定买不买快照相机,要看书架被翻的频率和管理员上书的频率——如果每分钟都有百人翻书,但一天只有几次上书,这相机就是神器;如果管理员们疯狂打架改书,那拍照也救不了场。

快照隔离最适合读多写少、事务短小、冲突率可控的系统。典型场景包括:电商商品详情页(大量浏览、少量下单改库存)、报表分析(长查询需要一致性视图)、多租户SaaS(各租户读自己的数据,互不干扰)。选型时,可以用一个简单模型估算重试成本。

系统上线后要监控两类指标:事务冲突率与回滚率。冲突率高意味着写热点严重,快照隔离带来的收益会被重试成本抵消;这时需要拆分热点、使用最终一致性改造,或升级到串行化快照隔离(SSI)。另外数据库中积压的不可见旧版本数量也值得监控,版本堆积会导致表膨胀、查询变慢,需配合自动VACUUM策略。

照片不是万能的,但用对地方就是效率神器。给读者多的馆配相机,给管理员打架的馆配排队门,正是数据库隔离级别选型的精髓。快照隔离把你从“读等写、写等读”的泥潭里拉出来,剩下的路,交给监控与调优。

Pretry=1−(1−p)n,n=TPS×ΔtP_{\text{retry}} = 1 - (1-p)^n,\quad n = \text{TPS} \times \Delta t
重试概率模型:p 是单次写冲突概率,n 是时间段内事务总数;重试概率随两者指数上升。
  1. 单次冲突率p 表示一次事务撞上写冲突的比例,由业务写热点决定。
    pp
  2. 事务总量n 是评估窗口内的总事务数,等于吞吐量乘时间长度。
    n=TPS×Δtn = \text{TPS} \times \Delta t
  3. 至少一次重试1-(1-p)^n 表示 n 次事务中至少出现一次冲突需重试的概率。
    1−(1−p)n1 - (1-p)^n

AI 应用场景

高并发电商商品详情页

阶段:数据层事务隔离设计

解决的问题:商品详情页被海量用户同时浏览,同时少量库存扣减在持续发生。传统两阶段锁下,读者读库存时锁住行,写者只能等待,高峰期页面响应变慢甚至超时。快照隔离让所有浏览请求基于一致性快照读取,互相不阻塞,库存扣减照常进行,页面吞吐量大幅提升。

具体案例:某电商平台大促期间,一个热门SKU被每秒上万次浏览,库存扣减每秒仅几十次。使用快照隔离后,详情页查询平均延迟从80ms降至15ms;遇到扣减冲突时,通过应用层重试补偿,库存扣减成功率保持在99.9%以上。

多租户SaaS报表系统

阶段:长事务一致性保障

解决的问题:报表生成需要跨多张表读取数据,若每张表都读“当前最新值”,报表生成过程中数据一变,最终报表前后不一致。快照隔离让整个报表事务只基于一张快照,所有表读数在同一时间点,报表天然一致。

具体案例:某SaaS平台每月为上千家租户生成财务汇总报表,月结任务需要扫描数百万行数据。使用快照隔离后,报表生成期间业务数据照常写入,报表与业务互不阻塞,月结时长从原有的6小时缩短到40分钟。

乐观锁驱动的订单预占

阶段:冲突检测与重试策略

解决的问题:秒杀场景下大量用户抢同一批商品,简单加锁会产生严重排队。快照隔离结合版本号乐观锁,让每个事务先读版本号再尝试更新,更新时检查版本号是否变化,冲突则快速失败并重试,替代长时间阻塞。

具体案例:某票务系统在演唱会开票瞬间涌入十万请求,系统用快照隔离读取余票快照,提交时校验版本号。冲突事务平均重试1-2次即成功,数据库行锁等待时间几乎降为零,系统在该峰值下稳定运行。

Python 演示

python58 行
class RowVersion:    """一行数据的一个历史版本。"""     def __init__(self, txid, data, prev=None):        self.txid = txid        self.data = data        self.prev = prev        self.committed = False     def commit(self):        self.committed = True  class InnoSnapshot:    """事务的快照:最大事务号 + 活跃事务集合。"""     def __init__(self, txid, active_txids):        self.txid = txid        self.active_txids = active_txids  def is_visible(version, snapshot):    """判断一个版本对某个快照是否可见。"""    if version.txid in snapshot.active_txids:        return False    return version.committed and version.txid < snapshot.txid  def find_latest_visible(head, snapshot):    """沿着版本链回溯,找到快照能看到的最近版本。"""    node = head    while node:        if is_visible(node, snapshot):            return node        node = node.prev    return None  def build_chain():    v1 = RowVersion(1, {"stock": 100})    v1.commit()    v2 = RowVersion(3, {"stock": 90}, prev=v1)    v2.commit()    v3 = RowVersion(5, {"stock": 80}, prev=v2)    v3.commit()    return v3  if __name__ == "__main__":    head = build_chain()    snap1 = InnoSnapshot(4, active_txids=set())    snap2 = InnoSnapshot(6, active_txids=set())    seen1 = find_latest_visible(head, snap1)    seen2 = find_latest_visible(head, snap2)    print("事务4看到的库存:", seen1.data["stock"])    print("事务6看到的库存:", seen2.data["stock"])    assert seen2.data["stock"] == 80    print("版本链可见性判断演示完成")
目的:用Python模拟MVCC版本链和快照可见性判断,演示不同事务在同一个版本链上看到不同历史版本的完整过程。
  1. version.txid in snapshot.active_txids快照拍照时如果版本作者还在场,这个版本不能出现在照片里。
  2. version.committed and version.txid < snapshot.txid已提交且编号更小,版本才有资格被看见。
  3. while node:沿着版本链往前倒退,像一本本翻旧修订寻找对的那本。
  4. snap1 = InnoSnapshot(4, active_txids=set())事务4进馆时拍照,相当于拿到当天书架的瞬间状态。
  5. assert seen2.data["stock"] == 80事务6必然看到最新已提交版本,验证可见性规则的正确性。

常见误区

误区我在学习隔离级别时,看到 PostgreSQL 的可重复读和快照隔离效果一样,就以为两者是同一个东西,面试时直接说“快照隔离就是可重复读”。

正解快照隔离是一种具体的并发控制技术,可重复读是 SQL 标准定义的一种隔离级别。两者只是“恰好常被实现成同一种行为”:PostgreSQL 的 REPEATABLE READ 默认用快照隔离实现,而 MySQL 的 REPEATABLE READ 在快照读之外还加了间隙锁来防幻读。正确的说法是“某数据库用快照隔离实现了可重复读语义”,而不是二者互为定义。

为何常见:主流数据库的行为高度相似,让学习者误以为标准和实现是同一概念,缺乏对“标准 vs 实现”层级的区分。

误区快照隔离不是说读写互不阻塞吗?那我把所有事务都设置成快照隔离,数据库就永远不会因为锁而等待了,并发问题全解决了。

正解快照隔离只是让读和写互不阻塞,写与写之间依然存在冲突。两个事务修改同一行时,后提交者会因“首次更新获胜”被中止或等待。读多写少时冲突少、收益大;写热点密集时,冲突率飙升,事务大量重试,整体性能反而可能不如加锁方案。

为何常见:把“读写互不阻塞”过度推广成“一切不阻塞”,忽略了写写冲突这个始终存在的约束。

误区两个事务改的是不同行,在快照隔离下应该各改各的、一定都能成功提交,不会互相干扰。

正解不一定。写偏斜就是反例:两个事务写入集不相交,却基于同一份旧快照的公共判断各自做修改,最终破坏业务约束。例如“账户总余额不能低于0”的两个子账户扣款,各自没超限,合起来却超了。检查数据完整性时,不能只看行冲突,还要看事务之间是否存在共享决策依赖。

为何常见:直觉上认为“冲突只发生在同一资源上”,忽略了逻辑约束这种看不见的共享资源。

误区快照隔离又高效又方便,那就把所有业务都切成快照隔离,串行化又慢又没用,根本没有存在的必要。

正解快照隔离存在写偏斜等异常,无法提供串行化保证。资金转账、库存上限、预约抢购等强约束场景,仍需要真正的串行化隔离。PostgreSQL 的 SERIALIZABLE 快照隔离(SSI)在快照基础上增加了读写依赖冲突检测,能在保持较高并发的同时阻止写偏斜,但它比普通快照隔离多了一层提交阶段检测。

为何常见:先体会到快照隔离的高并发甜头,容易忽略它“保证不了真正串行一致”的软肋,低估业务约束被破坏的代价。

面试题

Q1. 快照隔离中“读不阻塞写”的根本原因是什么?

单选题 · easy

  • A.读操作会获得共享锁
  • B.读操作使用历史快照← 正确答案
  • C.写操作不使用任何锁
  • D.读操作总是最后执行

答案:B。快照隔离下读事务通过MVCC读取已提交的历史版本,无需给当前数据加锁,因此写事务可以同时修改最新版本。A错误,快照读不获取共享锁;C错误,写者之间仍需要冲突检测;D错误,读写完全并行,不存在先后约定。

Q2. 下列哪项是快照隔离的主要实现技术?

单选题 · easy

  • A.行级锁技术
  • B.多版本并发控制← 正确答案
  • C.两级锁协议
  • D.时间戳排序

答案:B。MVCC通过保留多版本让读旧版、写新版共存,是快照隔离的底层支撑。行级锁和两阶段锁属于悲观并发控制,与“读写互不阻塞”的出发点相悖;时间戳排序是另一类并发控制方法,不是快照隔离的典型实现。

Q3. 事务T1写入行X,T2写入行Y,两者均读取了X和Y且从同一旧快照判断"X+Y≥100",最终X+Y=80。这属于?

单选题 · mid

  • A.脏读现象
  • B.写偏斜← 正确答案
  • C.丢失更新
  • D.幻读问题

答案:B。两个事务写入集合不相交,基于同一份过时快照做判断,最终违反公共约束,这是写偏斜的典型画面。脏读是读到未提交数据,丢失更新是后写覆盖先写,幻读是读到满足条件的新增行,均与题干不符。

Q4. 快照隔离最适合下面哪类业务负载?

单选题 · easy

  • A.读多写少← 正确答案
  • B.写多读少
  • C.读写各半
  • D.纯写负载

答案:A。读多写少时大量读请求不阻塞少量写请求,吞吐收益最大。写多场景下写写冲突频繁、重试成本高,且旧版本堆积导致表膨胀,快照隔离的优势会被抵消;纯写负载更适合行锁或串行化控制。

Q5. 快照隔离中仍然可能出现的异常有哪些?

多选题 · mid

  • A.写偏斜← 正确答案
  • B.脏读问题
  • C.只读异常
  • D.约束破坏← 正确答案

答案:AD。快照隔离能防脏读、脏写、丢失更新,但两个写事务在不同行上基于旧快照做决策时,会出现写偏斜并可能破坏约束,这是它无法保证串行化的关键证据。只读异常在快照隔离下不会出现,因为只读事务看到的是完全一致的快照。

Q6. 相比读已提交,快照隔离的优势有哪些?

多选题 · mid

  • A.多次读结果稳定← 正确答案
  • B.写偏斜会消失
  • C.避免不可重复读← 正确答案
  • D.同写一行无冲突

答案:AC。读已提交每条语句一个快照,同一事务内两次读可能结果不同;快照隔离整个事务一个快照,事务内多次读稳定,避免不可重复读。但快照隔离并不能消除写偏斜,写同一行的冲突依然存在,所以B和D错误。

Q7. 在快照隔离中,“首次更新获胜”指的是?

单选题 · hard

  • A.先发起更新的事务总是成功
  • B.先提交且未被覆盖者可成功← 正确答案
  • C.解锁更快的事务优先
  • D.回滚较少的事务优先

答案:B。两个写事务修改同一行时,先提交者赢得版本所有权;后提交者发现版本已变化,会被中止或等待。“先发起”不等于“先提交”,因此A错误;C和D描述的调度策略在快照隔离中并不存在。

Q8. 写偏斜的触发条件有哪些?

多选题 · hard

  • A.两事务写集合不相交← 正确答案
  • B.两事务读集合有交集← 正确答案
  • C.两事务更新完全相同行
  • D.提交后约束被破坏← 正确答案

答案:ABD。写偏斜的经典结构是写集合不相交(常规冲突检测发现不了)、读集合有交集(共享依赖旧快照)、提交后约束被破坏。若写集合有交集,就直接是写写冲突而不是写偏斜,因此C错误。

Q9. MVCC可见性判断中,版本v对事务T不可见的情况是?

单选题 · hard

  • A.v的创建事务编号小于T
  • B.v的创建事务编号等于T
  • C.v的创建事务编号大于T← 正确答案
  • D.v已提交且不在活跃集

答案:C。版本创建事务编号大于当前事务编号,说明版本诞生于当前事务快照之后,属于“未来版本”,照片里不应该出现。A和D是可见条件;B在全局递增事务号下不会出现在两个不同事务之间,若版本由T自己创建则对T可见,但不能推广为一般规则。

Q10. 快照隔离下,数据库无法保证以下哪个特性?

单选题 · easy

  • A.读数据是一致快照
  • B.写偏斜可以避免← 正确答案
  • C.读写互不阻塞
  • D.写写冲突可检测

答案:B。快照隔离保证一致快照、读写互不阻塞,也能检测写写冲突(后写者中止)。但写偏斜是它的已知漏洞,事务基于旧快照写不同数据行可能破坏约束,需要串行化快照隔离(SSI)来补足。