
一个疑问:同一行记录 反复修改该如何存储呢?为什么写入数据,却查询不出来
本文聚焦:OceanBase 数据库的 MVCC(多版本并发控制)机制
一句话解释:对同一个记录/位置多次修改,新增记录代替原地修改
快照链是现代存储系统(无论是本地、集中式还是分布式)的标准功能
带着问题去阅读:文件写入了一笔IO,为什么读取,却读不到?, 首先这个问题有歧义: 没有描述清楚:我写在哪里,怎么读的
开始
简单理解:快照有COW(Copy On Write,写时复制)和 ROW(Redirect On Write,写重定向)
建议:直接放弃,看不见摸不着 ,出问题了根本无法跟踪定位
•使用读多,写少情况,内存copy
•写入性能相对较低:在写入时,需要复制发生变化的数据块【频繁写入场景 不建议使用,例如文件系统千万OPS】
•举例 Redis的RDB快照正是利用了COW的思想来高效生成内存快照,避免了在fork()瞬间复制整个内存空间带来的巨大开销和停顿
•Redis的设计哲学是简单与高效。fork()加COW是一种利用操作系统原生能力、实现相对简单且高效的快照方案【直接使用系统os提供能力,自己不去实现】
•Redis不采用这种方式写重定向, ROW通常是存储系统(如SAN/NAS、分布式存储、文件系统)中针对磁盘数据块的快照技术。
ROW是底层存储系统用于实现磁盘卷多版本管理的空间优化型快照技术;
Redis RDB是内存数据库层面用于实现定期全量数据持久化的备份机制。
两者虽名含“快照”,但解决的问题域、实现原理和设计目标截然不同。
•ROW的目标是为磁盘卷提供瞬间的、空间高效的、可回滚的时间点副本,其管理的是物理存储空间。它的核心操作对象是磁盘上的数据块。
•Redis是一个内存数据库,其首要设计目标是将内存中的复杂数据结构(字符串、哈希、列表等)持久化到磁盘文件
强烈推荐,就是新增代替修改 类似LSM 全量 +增量
•ROW是底层存储系统用于实现磁盘卷多版本管理的空间优化型快照技术
•源卷(Source Volume):生成快照的原始存储卷或数据集。快照通常是源卷的副本,用于保护和管理源数据。
•目标卷(Target Volume):存储快照的位置,通常是一个单独的卷或存储空间。目标卷可以用于还原数据或创建新的副本。【新开辟一个空间】
•快照链(Snapshot Chain):多个快照按照时间顺序形成的链式结构。每个快照记录了一个特定时刻的数据状态,并链接到前一个快照,以支持数据恢复和管理。
•*只读快照:只读,不可变,用于数据保护和备份
•可写快照:
克隆:可写,基于快照,可写快照

快照链的核心机制:
1、读取链:当前快照 → 父快照 → 父快照,直到找到数据或到达链顶端
2、写入隔离:通过 SnapContext 确保写入只影响当前版本,不影响已有快照
3、如果多个快照链 读变慢,元数据需要分离。
为什么链过长会影响性能?—— 寻址开销
这就是关键所在。我们把链想象成一个需要层层拆开的包裹。
链很短(比如只有2-3层):要找一个数据,最多回溯2-3步就能找到,很快。
链很长(比如几十层):要找一个数据(尤其是读取一个未被频繁修改的旧文件),系统需要从最新的增量盘开始,一层一层地回溯检查几十个文件,才能确定数据到底存放在哪一层。
一句话描述:
块存储 对用户来说,对同一位置LBA多次写入,在快照链机制下,可以创建快照保护历史写入数据不会修改,
同样原卷继续写入,创建的快照也继续写入,他们写入不同空间,很简单磁盘空间很大,不考虑重复浪费问题。
MVCC,全称为多版本并发控制(Multi-Version Concurrency Control)
OceanBase 实现非常精妙,它通过在内存中维护数据行的多个版本来实现高并发的读写操作。
其核心可以概括为:
OceanBase 的 MVCC (多版本并发控制) 原理
数据结构: 行级版本双向链表(我理解成快照链)
算法:基于快照读写机制实现。
画外音:
提问1:MVCC 怎么与 OceanBase独特的“基线数据(SSTable) + 增量数据(MemTable)的LSM-Tree架构关联起来的? 提问2:LSM-Tree设计原来都有,MVCC原来也有,做了什没改造 将2这结合起。memtable内存数据结构图 提问3:其他产品设计MVCC有什么不同呢
下面这张流程图清晰地展示了OceanBase MVCC的核心运作机制,包括版本链的生成和读取过程:

下面我们来详细解读图中的关键环节和其背后的设计。
版本链
1、在 MVCC 机制下,数据库中的每行数据都关联着一个版本链。
2、版本链记录了该行数据的多个版本,每个版本都对应着特定的事务操作。
事务时间戳
每当一个新的事务开始时,它会获得一个唯一的、递增的时间戳。 这个时间戳代表了事务的开始时间,并且将被用于确定事务之间的先后顺序
读-写不阻塞
在MVCC机制下,读操作不会阻塞写操作,写操作也不会阻塞读操作。 这是因为读操作读取的是数据的历史版本,而写操作创建的是数据的新版本,两者不会相互干扰。
1、读一致性:读操作只能看到快照版本之前已提交的数据

ObMvccEngine是MVCC并发控制的核心引擎,负责协调所有读写操作(ob_mvcc_engine.h:52-64)
主要职责包括:
•创建KV对:通过create_kv()为每个键创建或获取对应的ObMvccRow
•Return the ObMvccRow according to the memtable key
•写入操作:mvcc_write()将新的事务节点插入到MVCC行中
•mvcc_write builds the ObMvccTransNode according to the arg and write into the head of the value
•读取操作:get()和scan()根据快照版本读取数据
•行压缩触发:在读取时通过try_compact_row_when_mvcc_read_()触发行压缩
ObMvccRow是存储同一行键所有多版本数据的核心结构,维护一个从新到旧的双向链表。(ob_mvcc_row.h:228-232)
ObMvccRow is the row contains all multi-version tx node for the specified
key, and all tx node is bidirectional linked and ordered with newest to
oldest.
关键字段:
•list_head_:指向最新的事务节点(链表头)
•latest_compact_node_:最近一次压缩产生的compact节点,用于优化后续压缩
•update_since_compact_:自上次压缩后的更新次数,用于触发压缩
•max_trans_version_:该行上已提交的最大事务版本
•total_trans_node_cnt_:事务节点总数 ob_mvcc_row.h:267-284

版本链在内存中的组织
ObMvccTransNode代表一个事务对某行数据的单次修改,存储增量数据(仅包含被修改的列)。
ObMvccTransNode is the multi-version data used for mvcc and stored on memtable.
It only saves updated columns for write and write by aggregating tx
nodes to data contains all columns.
核心字段:
struct ObMvccTransNode
{
transaction::ObTransID tx_id_; // 事务唯一标识
share::SCN trans_version_; // 事务提交版本号
share::SCN scn_; // 序列号/日志时间戳
transaction::ObTxSEQ seq_no_; // 事务内序列号
}
读操作通过 ObMvccAccessCtx 携带快照信息
ObMvccAccessCtx 是 OceanBase 中 MVCC(多版本并发控制)的核心上下文对象

1、使用 ObMvccAccessCtx 携带事务快照信息 ob_mvcc_acc_ctx.h:346-355
2、通过 ObMvccValueIterator 遍历事务节点链表
3、根据快照版本过滤可见数据
4、处理读锁冲突和事务集违反
假设有以下并发场景:
-- 事务T1 (snapshot_version = 100) BEGIN; UPDATE users SET name = 'Alice' WHERE id = 1; COMMIT; -- commit_version = 120 -- 事务T2 (snapshot_version = 110) BEGIN; SELECT * FROM users WHERE id = 1; -- 读取操作读操作流程:
1、初始化上下文:T2创建ObMvccAccessCtx,snapshot_version = 110
2、遍历版本链:
找到T1的修改节点(trans_version = 120)
比较:120 > 110,版本太新,跳过
继续查找更早的版本
3、可见性判断:找到trans_version = 90的版本,90 ≤ 110,可见
4、返回结果:返回T1修改前的数据 多版本读一致性介绍 https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000003977773

1、创建新的事务节点 (ObTxNodeArg)
2、调用 ObMvccRow::mvcc_write() 将节点插入链表头部 ob_mvcc_engine.h:80-89
3、处理写写冲突检测
4、通过 finish_kv() 使新版本对读操作可见
当update_since_compact_达到配置阈值时触发压缩: ob_mvcc_row.cpp:533-559
1、查找起始位置:从latest_compact_node_或list_head_开始,找到snapshot_version之前的所有已提交节点 ob_row_compactor.cpp:100-151
2、构建压缩节点:将多个增量节点合并为一个完整行节点
遍历版本链,读取每个节点的数据
使用列合并策略:后面节点的非NOP列会覆盖前面的
创建类型为NDT_COMPACT的新节点 ob_row_compactor.cpp:192-373
3、插入压缩节点:将compact节点插入到起始位置之后,更新latest_compact_node_指针 ob_row_compactor.cpp:378-400

MemTable在冻结时才确定最终的快照版本,这确保了快照版本包含了所有已提交的事务
•MemTable:是OceanBase数据库中用于存储内存中数据的数据结构。
•快照版本:每个MemTable都有一个关联的快照版本(MT_SNAPSHOT_VERSION),用于标记该MemTable中数据的最大可见版本。
•数据可见性:在读取时,系统会根据当前事务的版本号与MemTable的快照版本进行比较,判断哪些数据对当前事务可见。
•MemTable通过快照版本作为时间边界,确保事务隔离性。
•冻结时确定的快照版本包含了所有已提交事务,为读取操作提供一致性视图。这种设计在保证ACID特性的同时,实现了高效的并发访问。
ObFreezer负责MemTable的冻结管理:
通过设置freeze_flag_标记MemTable进入冻结状态
记录freeze_snapshot_version_作为该MemTable的快照版本上界

1、NOT_SET_FREEZE_FLAG:设置冻结标志
2、NOT_SUBMIT_LOG:提交冻结日志
3、WAIT_READY_FOR_FLUSH:等待所有未提交事务完成
4、FINISH:冻结完成,可以转储 ob_freezer.h:56-64
•新的写入会创建新的Active MemTable
•冻结的MemTable成为Frozen MemTable,只读
•resolve_snapshot_version_()确定最终的快照版本 ob_memtable.h:406-428
OceanBase采用LSM-Tree架构:
•MemTable层:内存中的可写表,包含Active和Frozen MemTable
•SSTable层:磁盘上的不可变表,通过Minor/Major Compaction生成
1、MemTable Freeze:MemTable被冻结后不再接受新写入
2、迭代器遍历:创建迭代器遍历Frozen MemTable中的所有行
按照行键顺序扫描
对每一行,遍历其版本链
3、版本过滤:只选择满足条件的版本写入SSTable
版本必须已提交
版本在freeze_snapshot_version之前
4、微块构建:将行数据序列化写入微块(MicroBlock)
使用ObCompatRowWriter进行行编码
多行数据聚合成微块
5、宏块组织:微块组织成宏块(MacroBlock),最终形成SSTable
在Minor Compaction中:
保留多版本:默认保留snapshot_version之后的所有版本 【同一个数据:保留多个版本的】
版本裁剪:已提交且早于snapshot_version的版本可以被裁剪【历史版本 过多了怎么处理】
Compact节点处理:NDT_COMPACT类型的节点作为完整版本保存
读操作的可见性规则:
可见条件: 1. 节点已提交(is_committed() == true) 2. 节点的trans_version <= snapshot_version 3. 节点不是当前事务的未提交修改 假设有以下场景:
事务T1(snapshot_version = 100)提交数据,trans_version = 90
事务T2(snapshot_version = 120)读取数据
事务T3(snapshot_version = 80)读取数据
可见性判断结果:
T2读取:90 ≤ 120,可以看到T1的数据 ✓
T3读取:90 > 80,看不到T1的数据 ✗
问:trans_version snapshot_version 区别?
trans_version:表示事务提交时分配的全局唯一版本号(也称作提交版本号),由时间戳生成器产生,具有单调递增特性。
snapshot_version:表示事务开始时获取的一致性快照版本,决定了该事务能看到哪些已提交的数据。
只有当数据项的 trans_version 小于等于事务的 snapshot_version 时,才说明该数据是在事务开始前或同时提交的,因此对该事务可见。
1、读写隔离:读事务通过snapshot_version只看到之前已提交的版本
2、写写隔离:通过行锁和写写冲突检测保证串行化
3、TSC检测:通过snapshot_version_barrier检测读后写异常
假设有一个表 t(id int primary key, name varchar(10)),初始为空。
时间 | 事务 | 操作 | 说明 |
|---|---|---|---|
T1 | Tx1 (tx_id=1001) | INSERT INTO t VALUES (1, 'Alice'); COMMIT; | 分配 trans_version=10001 |
T2 | Tx2 (tx_id=1002) | UPDATE t SET name='Bob' WHERE id=1; COMMIT; | 分配 trans_version=10002 |
T3 | Tx3 (tx_id=1003) | DELETE FROM t WHERE id=1; COMMIT; | 分配 trans_version=10003 |
此时,ID = 1 这一行对应的 ObMvccTransNode 版本链为(从新到旧):
Node | tx_id_ | trans_version_ | op_type_ | is_committed_ |
|---|---|---|---|---|
N3 | 1003 | 10003 | DELETE | true |
N2 | 1002 | 10002 | UPDATE | true |
N1 | 1001 | 10001 | INSERT | true |
现在启动一个新事务 Tx4,在其开始时获取 snapshot_version = 10002。
它执行查询:SELECT * FROM t WHERE id = 1;
1、从最新版本开始遍历 ObMvccTransNode 链。
2、检查每个节点是否满足可见性条件:
已提交?
trans_version <= snapshot_version?
不是当前事务的未提交修改?
trans_version=10003):10003 > 10002 → 不满足 trans_version <= snapshot_version
❌ 不可见,跳过
trans_version=10002):10002 <= 10002 ✅
已提交 ✅
不是当前事务 ❌
✅ 可见 → 返回 name='Bob'
✅ 最终结果:Tx4 看到的是
'Bob',符合快照一致性要求
•https://deepwiki.com/oceanbase/oceanbase
•https://deepwiki.com/search/oceanbase-mvcc_023fb0d8-85f8-4c55-a766-c9b994545857
•https://deepwiki.com/ceph/ceph
•https://asthenia0412.github.io/post/jie-jue-bing-fa-du-xie-dai-lai-de-kun-jing-%EF%BC%9A-duo-ban-ben-%2B-kuai-zhao-shi-xian-yi-zhi-xing-du.html
•https://www.oceanbase.com/obi