3.3 MVCC 与可见性判断


3.3 MVCC 与可见性判断

本节摘要:MVCC(多版本并发控制)是"读写互不阻塞"的根基:每行数据携带版本信息,每个事务按自己的快照判断哪一行可见。理解版本链与快照规则,膨胀、幻读、长事务三大疑难就都有了统一解释。

为什么读一份旧数据反而是对的

报表事务跑了四十分钟,期间业务改了三万行数据——报表应该看到哪一版?如果规定"看到最新",报表前后页数据就对不上;如果规定"看到开始时的那一份",报表就与写入完全隔离,谁也不等谁。后者就是 MVCC 的立场:写入产生新版本,读取按各自事务开始时的快照挑选版本,读与写在数据层面彻底解耦。代价是旧版本必须保留到"再也没人需要看它"为止——这又绕回了上一节的膨胀与清理。本章前两节的现象,根子全部在这套机制。

版本链:一行数据的多个化身

openGauss 里每行元组都带着隐藏字段:创建它的事务标识与删除(或被新版本替代)时的事务标识。一次 UPDATE 在逻辑上是"旧行标记为已删 + 新行插入",连续更新就串成一条版本链。判断某行对你是否可见,规则用一句话说:创建该版本的事务在你的快照之前已提交,且删除它的事务尚未提交或尚未发生——两个条件同时成立才可见。

两分钟验证一遍。开两个会话:

-- 会话一 BEGIN; SELECT balance FROM accounts WHERE id = 1; -- 看到 1000 -- 会话二此时执行:UPDATE accounts SET balance = 800 WHERE id = 1; 并已提交 SELECT balance FROM accounts WHERE id = 1; -- 仍是 1000:快照早于对方的提交 COMMIT; SELECT balance FROM accounts WHERE id = 1; -- 现在看到 800:新快照看到新版本

会话一在事务中反复读到的都是 1000,不是数据库"卡了",而是快照规则在如实工作。把这套规则刻进直觉后,很多"灵异现象"会自动消失:备份工具一致性问题、主从数据"不一致"的误报、应用里同一事务前后读值不同的疑惑,全是快照边界没对齐。

快照与隔离级别:规则的两档

可见性规则有一个可调的严格度,就是事务隔离级别。读已提交(默认):事务内每条语句都取新快照,所以上面会话一若在会话二提交后再查一次,会看到 800;可重复读:事务开始时取一次快照用到底,全程看到 1000。两档没有谁对谁错——读已提交换来更短的旧版本滞留,可重复读换来事务内读一致性,但拉长了旧版本存活时间、放大了长事务的清理压力。交付默认建议:应用明确需要事务级一致读(对账、导出)才用可重复读,并把这些事务的时长控制住;普通业务留在读已提交。

长事务:MVCC 体系的第一大敌

现在可以完整解释 3.1 节埋的伏笔了。清理线程回收旧版本的判断依据是"还有没有活跃快照可能需要它"——只要存在一个几小时不提交的事务,它快照之后产生的所有旧版本都不敢回收。于是形成经典事故链:一个忘提交的运维会话挂了一夜,第二天表体积翻倍、索引变厚、查询整体变慢,而所有人都在查"昨晚跑了什么大任务"。处置与预防:

-- 找出最老的活跃事务:它决定了旧版本能回收的边界 SELECT pid, usename, state, now() - xact_start AS xact_age, left(query, 50) AS sql_head FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start ASC LIMIT 5;

治理清单:给应用连接池设置事务级空闲超时;运维窗口强制"BEGIN 必须配对 COMMIT";对 xact_age 超过十分钟的事务设告警;报表与批量导出挪到备机跑,让长快照与主库清理互不干扰。

FAQ

为什么查了个 SELECT,表就膨胀了?

查询本身不产生版本,但它开启的事务持有快照。长查询 + 并发更新 = 大量旧版本滞留。解法不是不查,而是别让查询事务开着去干别的事。

备机上数据看起来和主机不一致,是复制坏了吗?

多数情况是"时间差 + 快照"的正常现象:你在主机和备机分别开事务的时刻不同,看到的版本边界就不同。核对一致性的正确姿势是用专门的校验工具在静止点比对,而不是肉眼跨库对数据。

可序列化级别什么时候用?

冲突极多且业务要求严格串行语义的场景。它的代价是冲突失败率上升、应用必须具备重试逻辑。多数业务用可重复读加重试就够,别轻易上可序列化。

实验课:亲手制造一次"不可见"

概念读三遍不如动手做一遍。在测试环境按这个剧本实验:第一步,建一张两行的小表,开两个终端会话;第二步,会话 A 开事务读一遍并保持不提交;第三步,会话 B 更新其中一行并提交;第四步,会话 A 再读——看到的仍是旧值;第五步,会话 A 提交后重读,新值出现。第六步,改变剧本:会话 A 的隔离级别换成可重复读,重复第四、五步,对比两次实验的差异。做完这六步,快照、隔离级别、可见性三个概念就从"背下来的"变成"见过的"。最后一个加分实验:在会话 A 长开的同时开第三个会话持续更新那行一千次,然后观察系统视图里的死元组计数——你会亲眼看到"长事务冻结回收"不是文档恐吓,而是机制使然。

与备份、复制的联动:MVCC 的下游效应

MVCC 的快照机制是多个周边能力的地基,理解这条联动线能一举多得。备份工具的一致性快照:物理备份要保证数据文件的同一时刻视图,靠的正是快照与日志配合;逻辑导出的一致性同理——导出期间其他会话的提交不会混进导出结果。主备复制:备机回放日志产生与主机一致的版本历史,备机上的查询同样按快照判断可见性,这正是"备机读到的数据与主机有合理时间差"的机制根源(第 5 章展开)。长事务危害的普适性也因此成立:无论备份、复制还是清理,都建立在"旧版本在某个边界前必须保留"的约定上,超长事务把这个边界越拉越长,所有下游机制跟着遭殃。看懂这条联动线,你就明白为什么资深 DBA 看到超过小时的闲置事务会直接动杀心。

前沿视角:MERGE 与写倾斜

隔离级别的边界问题里,写倾斜是最隐蔽的一类:两个事务各自更新不同的行,单看每行都不冲突,但合起来违反了业务不变量——经典场景是值班排班,两个事务分别检查"当前值班人数大于一"后各自请假,两个检查都通过,最终值班台空了。读已提交与可重复读都拦不住它,严格的可序列化级别才能捕获这种跨行约束。识别方法:找到业务里"先查条件再据条件写入"的代码模式,它就是写倾斜的温床。工程解法按代价排序:给条件涉及的行加显式锁(SELECT 加排他意向)把隐藏冲突显式化;用带条件更新替代"先查后写"(一条 UPDATE 带上业务条件,让内核原子判定);最后才是升级隔离级别加全面重试。某排班系统上线后偶发"值班台空档",按第二解法改造后归零——改动一行 SQL。

可见性规则的三个推论

机制讲完,把规则往下推三步,得到三个实用推论。推论一,事务里读不到自己事务之外的任何"未来",即使那条事务随后回滚了也一样——回滚的版本照样占过位置,这就是为什么高写入并发的库需要更强的清理能力。推论二,只读事务同样产生快照成本——大导出期间旧版本滞留的根源是快照而不是写,所以"我们只是查一下"从来不是开长事务的理由。推论三,可见性判断是逐行进行的,查询扫过的每一行都要做一次判断——这意味着可见性开销与扫描行数成正比,这也是为什么索引扫描(扫的行少)在 MVCC 体系里不只是省 IO,还省判断。三个推论都属于"知道了会在某个瞬间恍然大悟"的类型:将来某个时刻你面对一个反直觉的现象,它们中的一个会替你解围。

本节要点回顾

  • 一句话机制:更新留版本链,读取按快照挑版本,读写因此在数据层解耦;
  • 可见性两条件:创建事务先于快照已提交,删除事务未提交或未发生;
  • 隔离级别是快照频率:读已提交逐句快照,可重复读全程一快照;
  • 长事务冻结回收:最老活跃事务的快照就是清理边界,治理长事务等于治理膨胀;
  • 默认级别别乱动:改隔离级别前先确认应用的事务模型与重试能力。

读写互不阻塞了,那写与写撞在一起怎么办?下一节拆锁管理、等待队列与死锁日志。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U