想象你和一个朋友合租,厨房冰箱公用,谁都能做饭。但如果没有规矩,麻烦会像滚油一样溅开:一个室友炒菜到一半跑去接电话,油锅还烧着;另一个室友把他切好的菜倒进去,两人一搅和,谁的菜都毁了,冰箱里还少了食材。于是你们坐下来,定了一本《公共厨房公约》。 公约一共四条铁律:第一,做饭必须一气呵成——要么端上桌,要么中途搞砸了就把厨房恢复原样,仿佛你从没来过(这正是原子性);第二,冰箱里的食材只能有借有还,账目永远守恒,不能凭空多出三斤肉(这是一致性);第三,两个人同时做饭时互不干涉,你只管按自己开火那一刻看到的厨房来操作(这是隔离性);第四,每次拿放食材都要先在一本永久流水账本上记一笔,哪怕半夜停电,第二天照着重做一遍(这是持久性)。 数据库世界的“事务”(Transaction),就是这套厨房公约。它背后的两大功臣,一个叫 undo 日志(撤销记账本),一个叫 redo 日志(永久流水账本),两者又合力撑起了 MVCC 多版本并发控制。这篇文章就借这间厨房的烟火气,把 ACID 的每个角落、每条实现细节,通通讲透。
给 AI 从业者的话:模型训练记录、特征库更新、用户行为落库,数据可靠性差一步,整个系统的可信度就崩一步。搞懂事务与 MVCC,是你从“会调接口”进阶到“能设计健壮数据系统”的分水岭。
目录
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
事务概念基础——一件事的完整仪式
室友阿明今晚要做鱼香肉丝,从开冰箱拿肉、切菜、开火、爆炒、上桌,到最后一并洗碗收拾,这是一套不能拆开的完整仪式。任何一步半途而废,这顿饭就不成立。数据库里,这个“完整仪式”就叫事务(Transaction):把多条 SQL 语句打包成一个逻辑单元,要么一起成功,要么一起失败,不允许只成功半截。
为什么需要打包?最经典的例子是转账:从张三账户扣 100 元,同时给李四账户加 100 元。这两条 SQL 必须绑定在一起。如果只扣不加,钱就凭空消失了;只加不扣,银行就得破产。数据库用事务把“扣”和“加”焊成一块,谁也别想拆开。
在工程上,事务以 BEGIN 或 START TRANSACTION 开始,以 COMMIT(提交,确认结果永久生效)或 ROLLBACK(回滚,撤销全部操作)结束。它的一生有明确规定:执行中、执行完待提交、已提交;或执行中、失败、已回滚。MySQL 的 InnoDB 引擎会在执行每条写操作时顺手把旧值记入 undo 日志,为随时回滚做好准备。
事务的开端,就是阿明系上围裙的那一瞬间。数据库从此记住:这个人要么完整地做完一顿饭,要么把厨房恢复原样。
- ACTIVE 状态事务刚开始执行,正在处理业务操作,就像阿明刚把肉从冰箱里拿出来,还没下锅。
- PART_COMMIT 状态所有 SQL 都执行完,但还没写最终确认,相当于菜已出锅,就差端上桌那一刻。
- COMMIT 路径提交成功,整个事务永久生效,菜上桌了,洗碗收盘都按规矩来。
- ABORT 路径执行中出现失败,事务中止并回滚,一切恢复到开始之前。
原子性原理——全有或全无的烟火
锅里已经开始冒烟,阿明发现酱油瓶是空的,油温又太高。他当机立断:这顿饭不做了。于是把肉放回冰箱、菜倒回袋里、锅刷干净、台面抹平——厨房恢复到开火之前的样子,仿佛一切都没发生过。这就是原子性(Atomicity):全有或全无,不允许中间状态。
数据库里,原子性意味着一个事务中的所有操作被视为一个不可分割的整体。要么全部执行成功并留下结果,要么全部不执行,数据回到事务开始前的状态。绝不存在“转账扣了款,但入账失败”这种半吊子结果。它像一道闸门:整道工序不过关,就把闸门后面所有东西冲回原点。
支撑这道闸门的,是 undo 日志(撤销日志)。InnoDB 在每条写操作执行前,会把旧值记入 undo 日志,形成一条“逆操作链”。回滚时,数据库沿着这条链从后往前逐个执行逆操作:INSERT 的逆操作是 DELETE,UPDATE 的逆操作是恢复旧值,DELETE 的逆操作是重新插入。旧值链有多长,回滚就能走多远。
阿明凭着一张“撤销备忘清单”把厨房恢复原样。数据库的 undo 日志,就是那张清单——不是靠记忆,而是靠逐条记录的旧值,把烟火气一点点抹干净。
- S_after 最终状态事务结束后数据库所处的状态,也就是我们实际能看到的数据模样。
- Abort(T) 中止事务事务执行过程中发生失败,主动或被动进入中止流程。
- UndoAll 回滚全部按照 undo 日志从后往前执行所有逆操作,把每一步的旧值都还原回去。
- 等于 S_before回滚完成后,数据库状态与事务执行前完全一致,旧值链被完整消耗。
一致性保障——账目永不失衡
公寓公约里有一条硬规矩:公共冰箱里的东西必须账实相符。阿明拿走一瓶酱油,就得在记账板上写“借酱油一瓶”;用完放回来再划掉。如果谁拿了东西却不肯记账,月底一盘点,账就平不了。数据库的一致性(Consistency)讲的就是这个:无论事务怎么折腾,数据必须从一种合法状态变到另一种合法状态,不允许出现“负余额”这种荒谬账目。
这些合法性由两类规则定义:一类是数据库内置的完整性约束——主键不能重复、外键必须存在、字段不能有空值;另一类是业务规则——转账总量不变、库存不能为负、余额不能透支。前者数据库自动强制,后者写进应用逻辑。事务提交后,所有规则必须同时成立。
一致性不是某一个机制单独实现的,而是“原子性 + 隔离性 + 应用正确性”共同作用的结果。原子性保证中途失败不会留下残缺账目;隔离性保证其他事务看不到还没算完的中间账;而业务的正确逻辑保证约束本身被满足。四者合在一起,才凑齐了“账永远平”这一结果。它也解释了为什么一致性常被称作 ACID 中“最含糊”的一条——它更像一个目标,而不是一种具体机制。
账目守恒的公约,从来不是靠单独一条规矩生效的,而是所有规矩形成合力。一致性也一样——它是数据库约束与业务规则共同守卫的那杆秤。
- Consistent(S) 一致状态数据库处于某种状态 S,称其一致当且仅当它不违反任何规则。
- C_i(S) 单条约束第 i 条约束(主键、外键、非空、唯一、业务规则)在当前状态下成立。
- 合取符号对 i 从 1 到 n 全部取“且”,任何一条不满足,整体就不一致。
隔离性级别——厨房的四扇门
公寓里常有两个人同时想做饭。为了避免互相泼油、抢灶台,你们设计了四种厨房使用模式,由紧到松:第一种全开放,谁都能进来随便看甚至用半成品,这叫读未提交;第二种半开放,能看到别人橱柜里最终的成品,但看不到还没做完的锅,这叫读已提交;第三种个人包间,关上门后你看到的一切自始至终不变,门外的动静与你无关,这叫可重复读;第四种严格排队,一人做完下一位再进,这叫串行化。
隔离性(Isolation)就是这个意思:多个并发事务各自看到的数据世界不能互相干扰。标准 SQL 定义了四个隔离级别,每个级别解决一些并发问题,又留下另一些问题。读未提交可能读到别人没提交的脏数据(脏读);读已提交解决了脏读,但同一事务内两次查询结果可能不同(不可重复读);可重复读保证事务内读到的数据始终一致,但另一个事务插入新行,导致查询多出数据(幻读);串行化把一切并发都排成队,彻底消除异常,代价是吞吐量骤降。
MySQL InnoDB 默认使用可重复读,但它并非放任幻读不管——它靠 MVCC 快照读屏蔽了幻读的“读半边”,再用间隙锁(Gap Lock)管住“写半边”。也就是说,四个标准级别是通用分档,具体引擎可以在同一档位上做出更强的保证。
厨房选哪扇门,取决于今天是想快速做个简餐,还是想安静地招待客人。数据库选隔离级别,就是在“做菜效率”和“不被打扰”之间,挑一扇恰到好处的门。
- RU 读未提交最松的级别,事务能读到其他事务未提交的数据,脏读随时可能发生。
- RC 读已提交只能读到已提交的数据,脏读被禁止,但两次读的结果可能不一致。
- RR 可重复读事务内第一次读之后,之后的读到的是同一个快照,不可重复读被阻止。
- SR 串行化事务完全排队执行,等价于没有并发,所有异常都被消灭。
持久性机制——永久流水账本
公寓有一次半夜跳闸,冰箱的凉气跑光,刚买的肉差点坏掉。好在你们有一本永不撕页的“永久流水账本”:谁在什么时间把什么食材放进冰箱,每一笔都记得清清楚楚。第二天照着账本,把食材挪进备用冷柜,损失被完全挽回。持久性(Durability)正是这本账本的意义——事务一旦提交,修改就永久保存,哪怕断电、崩溃,数据也绝不丢失。
数据库靠两条腿走路:一条是 undo(撤销),一条是 redo(重做)。redo 日志是物理日志,记录“数据文件中第几页、哪个偏移量、被写成了什么字节”,精确到细胞的病历。事务提交前,数据库先把这批 redo 记录落盘,再动数据页面本身——这就是传说中的 WAL(Write-Ahead Logging,预写日志)。先记账、后干活,账本永远比现场多一步。
如果提交后系统崩溃,但数据页还没来得及写进磁盘怎么办?崩溃恢复时,数据库扫描 redo 日志,把已提交但未落盘的操作重放一遍,就像对照账本把食材重新归位。因为“先写日志再写页”的顺序保证,日志里有的必然能恢复。redo 管“重做”,undo 管“撤销”,一个向前救火,一个向后灭火,方向正好相反,分工互不重叠。
厨房里最值钱的不是食材本身,而是那本怎么撕都撕不烂的流水账。数据库的 redo 日志也一样——只要账本还在,任何灾难都只是重做一遍的事。
- Commit(T) 提交成功事务到达提交点,应用层向用户宣布“这笔操作成功了”。
- 蕴含符号表示“必须推出”——提交成立的前提是后面这句话为真。
- fsync(redo_log) 日志刷盘把 redo 日志从内存缓冲区强制写入磁盘,确保断电不丢。
- 先于写数据页数据页刷盘必须发生在日志刷盘之后,用"≺"表示严格的时间先后。
MVCC多版本——各看各的历史快照
阿明和阿强同时在厨房做菜。阿明开火时看到冰箱里鸡肉剩 500 克,阿强晚五分钟开火,看到的也是那 500 克。两人各拿各的“进门快照”,互相不干扰。可阿明中途炖掉 100 克鸡肉,阿强并不知道,也不影响他按自己的快照做菜。这就是数据库的多版本并发控制(MVCC,Multi-Version Concurrency Control)——让每个事务活在属于自己的“厨房照片”里。
老派的做法是锁:读的时候锁住数据不让写,写的时候锁住数据不让读,读和写互相堵车。MVCC 换了个思路:同一行数据不只存一个当前值,而是通过 undo 日志保留一串历史版本。写操作生成新的“当前版本”,旧版本不删除,而是挂在一条版本链上;读操作去读某个合适的旧版本,压根不用等写操作完成。于是读不阻塞写,写不阻塞读,两条车道各自通行。
在 InnoDB 中,每行记录头都藏着两个隐藏列:trx_id(生成这个版本的事务 ID)和 roll_pointer(回滚指针,指向 undo 日志里的上一个版本)。所有版本通过 roll_pointer 串成一条完整的版本链,最新版本在链头,最古老的在链尾。不同事务根据自己的“快照”决定从链上取哪个版本。
版本链就是厨房里的整本“老照片档案”。想看清旧厨房的模样,翻出对应日期的照片就行,完全没必要把现在桌上的菜端走。MVCC 的智慧就在这三言两语之间。
- Version(r) 数据行版本一行数据 r 的某个历史版本,记录着当时的数据内容及其元信息。
- trx_id 生成者创建这个版本的事务编号,用来在可见性判断中对号入座。
- roll_ptr 回滚指针指向 undo 日志中构造上一个版本的记录,形成链表结构。
- 指向上一个版本箭头表示从当前版本沿着指针找到历史前驱,链不断往前追溯。
可见性判断——按号码牌认账
阿明拿着进门时拍的快照,想知道“冰箱里的鸡肉到底应该看哪个版本”。光有快照不够,还得有一份号码牌名单:哪些室友在我进门时已经在厨房里忙活(事务还在活跃),哪些早就做完走了(事务已提交)。号码牌一对,该认账还是不该认账,一清二楚。
这份号码牌名单在数据库里叫 ReadView(读视图),它记录四个关键值:creator_trx_id(生成视图的事务 ID,即“我自己”)、min_trx_id(活跃事务中最小的 ID)、max_trx_id(下一个将被分配的事务 ID,即上界)、active_ids(当前所有活跃事务的 ID 集合)。拿到一个历史版本,看它的 trx_id 是谁生成的,再用下面的规则判断是否对当前事务可见:
ReadView 的生成时机决定了隔离级别的行为差异:读已提交(RC)每次 SELECT 都会生成新视图,所以两次查询可能看到不同的最新提交;可重复读(RR)只在事务第一次查询时生成一次视图,之后一直复用,所以整个事务看到的是同一个冻结的厨房。号码牌名单一旦定下,厨房的景象就凝固了。
- t 为版本生成者待判断版本是由事务 t 创建生成的,t 是该事务的 ID。
- t < m 已结束的事务t 比所有活跃事务都小,说明它的工作早已完成提交,当前快照必然可见。
- min ≤ t < max 区间内t 落在活跃区间内,进一步要查它是否还活跃。若
t ∉ A,说明已经提交,可见。 - t = c 自己可见如果版本是自己生成的,当然能看见自己写的数据。
- 三种情况取“或”三个条件只要满足任意一个,版本就可见;全部不满足则不可见。
工程权衡——完美公约的代价
如果把厨房四壁都加上防弹玻璃,每个室友完全隔离,安全是安全了,可谁都进不去做饭了。数据库也一样:隔离级别拉到串行化,并发全部排成队,吞吐量断崖式下跌。工程上从不存在“绝对安全”的免费午餐,只有“够用即可”的取舍。
MVCC 也不是万能药。它解决的是读与写之间的冲突,让两边互不阻塞;但两个事务同时改写同一行,写与写之间的冲突仍然存在,绕不开锁或版本号。更隐蔽的是快照隔离下的丢失更新:两个事务都基于同一旧快照读到了库存 100,各自减 1,先后提交,结果库存变成 99 而不是 98——其中一个更新被静默吞掉了。
所以工程师的真实工作,是学会选门和补漏:读多写少的长查询业务,用可重复读 + MVCC 性价比最高;金融转账、库存扣减这类强一致场景,就要上显式锁、乐观锁甚至串行化。给公寓选门的智慧,从来都是“刚刚好”,而不是“最安全”。
- 并行写同一数据W₁ 和 W₂ 同时修改 x,"∥"表示两者并发执行。
- 最终值为后写者并发写的结果是后提交事务的版本 v₂。
- 先写值丢失v₁ 被覆盖,如同没写过,导致业务上的一次更新静默消失。
AI 应用场景
金融交易系统:一次转账的生死时速
阶段:数据存储与管道(可靠数据落库)
解决的问题:传统转账由“扣款 + 入账”两条 SQL 组成,任何一条失败都会造成账目不平。AI 风控系统实时判定的结果(如冻结银行卡)也需原子写入——判定成功但落库失败,诈骗交易就漏过去了。
具体案例:在银行核心系统中,每次转账都在一个事务内完成。若程序在中途崩溃,undo 日志自动回滚扣款;若转账成功但机房断电,redo 日志在重启后把已提交数据重放出来。AI 反欺诈系统写入“冻结标记”时同样要走事务,确保风控结论和资金操作同生共死。
电商秒杀:库存不被超卖的 MVCC 大战
阶段:高并发在线推理与状态写入
解决的问题:秒杀场景下成千上万个请求同时扣库存,如果各自读到旧快照,就会发生超卖。MVCC 的快照读让查询不排队,但写写冲突必须用行锁或乐观锁版本号兜底。
具体案例:一个 AI 推荐系统在用户点击“立即抢购”时,先通过快照读快速判断库存,再对库存行执行带版本号的 UPDATE ... WHERE version = ?,更新失败则重试。把 MVCC 当作“读优化器”,把乐观锁当作“写守门员”,两者配合让秒杀系统既高效又不超卖。
AI模型训练:checkpoint 的原子保存
阶段:模型训练与持久化(MLOps)
解决的问题:大模型训练几十小时,中途断电就前功尽弃。定期保存的 checkpoint 必须原子性写入——写一半的文件等于没有。
具体案例:PyTorch Lightning 保存模型时,先把权重写进临时文件,再通过原子改名替换旧文件。这套思路和数据库事务同源:临时文件是“未提交状态”,改名是“提交点”,一旦崩溃恢复,旧 checkpoint 仍然完好。若把训练元数据(超参数、指标曲线)存进数据库,同样需要事务保护,防止指标表和参数表对不上账。
Python 演示
class SimpleTxnDB: def __init__(self): self.data = {} # 数据表: key -> value self.undo_log = [] # 撤销日志: [(key, old_value)] def set(self, key, value): old = self.data.get(key) self.undo_log.append((key, old)) self.data[key] = value print("SET {} = {} | undo_log_len = {}".format(key, value, len(self.undo_log))) def rollback(self): while self.undo_log: key, old = self.undo_log.pop() if old is None: self.data.pop(key, None) else: self.data[key] = old print("rollback done, data = {}".format(self.data)) def commit(self): self.undo_log.clear() print("commit done") def mvc_visible(trx_id, min_id, max_id, active_ids, creator_id): if trx_id < min_id: return True if trx_id >= max_id: return False if trx_id not in active_ids: return True return trx_id == creator_id if __name__ == "__main__": db = SimpleTxnDB() db.set("alice", 100) db.set("bob", 0) db.set("alice", 70) db.set("bob", 30) db.rollback() assert db.data == {}, "atomicity: all changes undone" assert mvc_visible(1, 2, 5, [2, 3], 4) is True # t < min_id assert mvc_visible(4, 2, 5, [2, 3], 4) is True # t == creator assert mvc_visible(3, 2, 5, [2, 3], 4) is False # t in active_ids assert mvc_visible(5, 2, 5, [2, 3], 4) is False # t >= max_id print("MVCC visibility rules passed")
self.undo_log.append((key, old))每次写之前先把旧值存入撤销日志,这是“先记账后干活”。while self.undo_log:从日志尾部开始逐个弹出,执行逆操作恢复旧值。if old is None: ... else:旧值为空表示原本没有该键,回滚时删除;否则恢复原值。db.rollback()一次回滚把四次set全部撤销,assert db.data == {}验证原子性。mvc_visible(1, 2, 5, [2, 3], 4)用具体数字验证可见性规则的四条分支:小于最小ID可见、自己是创建者可见、活跃列表不可见、大于上界不可见。
常见误区
误区我一开始以为 ACID 里的原子性就是编程课里讲的 CAS、自旋锁那种“单条指令不可分割”,所以只要用 synchronized 包住事务就万事大吉了。
正解这两个“原子”完全是两码事。多线程的原子操作是底层 CPU/内存层面的“一次性完成”;事务的原子性是逻辑层面的“一组操作要么全成要么全无”,由 undo 日志在失败时逐条回滚实现。事务可能跑了上百条 SQL,物理上绝不是一瞬间的事,但有日志兜底,最终效果如同没做过。
为何常见:同一个词在不同领域各有一套含义,而大部分入门教程先教并发编程,容易把先入为主的印象带到数据库里。
误区串行化最安全,脏读、不可重复读、幻读全都没有,那把数据库隔离级别全部设成 SERIALIZABLE 不就好了?
正解串行化意味着事务完全排队执行,并发度几乎归零。一个秒杀系统如果用串行化,请求会排成长龙,吞吐量断崖式下跌。现实中绝大多数业务用读已提交或可重复读就能满足需求,只需用锁或版本号处理少量关键写冲突。隔离级别是正确性和性能之间的杠杆,不是越高越好。
为何常见:教材通常只讲“高级别堵住了哪些异常”,不强调成本,容易让人产生“越严越好”的线性错觉。
误区MVCC 让读写互不阻塞,那是不是数据库就可以完全抛弃锁了?以后并发随便写就行。
正解MVCC 只解决了“读写冲突”,让读不阻塞写、写不阻塞读。但“写写冲突”依然存在:两个事务同时改同一行,需要一个排队机制,否则会互相覆盖。此外快照隔离下还可能发生丢失更新——两个事务读同一旧快照各自提交,后提交者吞掉先提交者的结果。MVCC 不是免死金牌,只是把读车道的拥堵挪开了。
为何常见:“读写互不阻塞”的宣传听多了,容易忽略写与写之间永远存在的竞争关系。
误区undo 能回滚,那 redo 应该也能回滚吧?或者 redo 就是数据库的备份文件,用来恢复误删数据的。
正解方向正好相反。undo 是“撤销日志”,管事务回滚和 MVCC 历史版本构建,记的是逻辑逆操作;redo 是“重做日志”,管崩溃恢复时把已提交但没写盘的操作重放一遍,记的是物理页修改。一个向后灭火,一个向前救火,职责完全不同,也互不替代。
为何常见:两个名字只差一个字母,讲课时又常放在一起对比,初学阶段极易混淆。
误区可重复读下同一个事务里两次 SELECT 结果完全一样,所以幻读已经彻底不存在了,不用再管。
正解MVCC 快照读确实能屏蔽掉大多数幻读,但“当前读”(SELECT ... FOR UPDATE、UPDATE、DELETE)读的是最新版本,仍可能看到其他事务新插入的行。MySQL InnoDB 在可重复读下额外用间隙锁(Gap Lock)解决当前读的幻读,这不是标准 SQL 隔离级别自带的属性,而是引擎的加强措施。
为何常见:只盯着快照读的行为,忽略了“读最新数据”的那条路径,就把“堵住了一半”当成了“彻底堵住”。
面试题
Q1. 关于事务(Transaction)的定义,最准确的是?
- A.一组要么全部提交要么全部回滚的操作← 正确答案
- B.一条能够在数据库正常运行的SQL语句
- C.一种用于加密数据库敏感数据的工具
- D.一个存储数据库备份数据的物理文件
答案:A。事务是把多条 SQL 打包成一个逻辑单元,具备全成或全无的特性。单条 SQL 只是事务的组成元素,加密、备份则是完全无关的数据库能力。A 抓住了“打包 + 全成或全无”的本质。
Q2. 原子性(Atomicity)的核心含义是?
- A.事务内操作要么全部成功,要么全部恢复← 正确答案
- B.多个事务之间互相隔离,互不干扰对方
- C.事务一旦提交,数据永久保存不会丢失
- D.事务执行前后,数据总量严格保持不变
答案:A。原子性是“全有或全无”,靠 undo 日志回滚支撑。B 是隔离性,C 是持久性,D 是一致性的一种表现。四个选项恰好对应四个特性,是最经典的区分题。
Q3. 数据库实现事务回滚的关键机制是?
- A.redo日志的崩溃重放过程
- B.undo日志的逆操作链← 正确答案
- C.数据页的双写缓冲区机制
- D.主键索引的B+树结构
答案:B。undo 日志记录了每次写操作前的旧值,回滚时沿着逆操作链从后往前恢复,就像按备忘清单把厨房恢复原样。redo 用于崩溃后“前滚”,双写缓冲和 B+ 树与回滚无直接关系。
Q4. 在读已提交(READ COMMITTED)隔离级别下,不会出现的现象是?
- A.脏读,读到未提交事务的临时数据← 正确答案
- B.不可重复读,两次查询结果不相同
- C.幻读,查询结果的行数凭空增减
- D.更新丢失,后写覆盖先写的提交
答案:A。读已提交每次查询都生成新的 ReadView,未提交的数据一律不可见,因此堵住了脏读。但不可重复读和幻读在标准定义下仍可能发生;更新丢失属于快照隔离下的写并发问题,也不由该级别直接解决。
Q5. MVCC 中 ReadView 的主要作用是?
- A.决定当前事务可见哪些历史版本← 正确答案
- B.保存数据表所有物理块的地址
- C.锁住版本链上所有旧版本数据
- D.记录redo日志的最新LSN序号
答案:A。ReadView 相当于进厨房时领到的号码牌名单,记录活跃事务集合,用来判断版本链上哪个版本对当前事务可见。它不存地址、不加锁、也不管 LSN。
Q6. 可重复读(REPEATABLE READ)与读已提交相比,核心区别是?
- A.快照在首次查询生成且整个事务复用← 正确答案
- B.每次查询都重新生成全新的快照视图
- C.完全不使用锁和版本链机制
- D.允许读到其他事务未提交的数据
答案:A。可重复读在事务第一次 SELECT 时生成 ReadView 并一直复用,所以事务内每次读到一致快照;读已提交每次查询生成新视图。C 说法错误,RR 仍用锁和版本链;D 是读未提交的特征。
Q7. 事务 T 生成 ReadView:m=20,M=30,活跃列表 A={21,25,28}。版本 v 由 trx_id=25 生成,v 对 T 是否可见?
- A.可见,因为25大于最小活跃ID20
- B.不可见,因为25在活跃事务列表中← 正确答案
- C.可见,因为25等于当前事务自身ID
- D.不可见,因为25小于最小活跃ID20
答案:B。25 落在 [20, 30) 区间内,且在活跃列表 A 中,也不是事务 T 自己,所以版本生成者尚未提交,当前事务不可见该版本。这对应核心公式中的第二条分支“t∈A 且 t≠c ⇒ 不可见”。
Q8. 事务的 ACID 四特性包括以下哪几项?
- A.原子性、一致性、隔离性、持久性← 正确答案
- B.可用性、分区容错性、原子性、隔离性
- C.一致性、隔离性、持久性、可扩展性
- D.原子性、持久性、聚集性、规范性
答案:A。ACID 是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个英文首字母的缩写。B 混淆了分布式系统的 CAP 定理,C 的“可扩展性”和 D 的“聚集性、规范性”都不属于 ACID。
Q9. 关于 redo 与 undo 日志,下列说法正确的是?
- A.undo 用于事务回滚和构建历史版本← 正确答案
- B.redo 用于崩溃恢复时重放已提交数据← 正确答案
- C.undo 是物理日志,记录页的字节修改
- D.redo 与 undo 的恢复方向正好相反← 正确答案
答案:ABD。undo 是逻辑逆操作链,负责回滚和 MVCC 旧版本构建;redo 是物理日志,负责崩溃后前滚重放。方向上一个向后撤销、一个向前重做,正好相反。C 说反了——记录“页字节修改”的是 redo。
Q10. MySQL InnoDB 在可重复读隔离级别下,仍可能出现的并发异常场景是?
- A.两个事务读同一快照后写入覆盖彼此← 正确答案
- B.当前读 INSERT……FOR UPDATE 检测到幻读← 正确答案
- C.两个事务并行修改不同行并等待行锁
- D.两个事务同时更新同一条记录发生死锁
答案:AB。可重复读的快照读能挡住部分幻读,但当前读仍需要间隙锁来防幻读,B 是可能出现的;快照隔离下的丢失更新(A)也依然存在。C 中修改不同行通常不冲突,不构成异常;D 中的死锁是锁机制的正常调度现象,并非数据异常。