首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深入理解数据库子系统(1) OceanBase MVCC设计原理(1.2万字)

深入理解数据库子系统(1) OceanBase MVCC设计原理(1.2万字)

作者头像
早起的鸟儿有虫吃
发布2026-01-14 15:34:42
发布2026-01-14 15:34:42
1700
举报

一、对你有什么帮助

一个疑问:同一行记录 反复修改该如何存储呢?为什么写入数据,却查询不出来

本文聚焦:OceanBase 数据库的 MVCC(多版本并发控制)机制

一句话解释:对同一个记录/位置多次修改,新增记录代替原地修改

二、依赖知识:文件系统快照 Snapshot基本特点

2.1 了解基本系统设计原理

快照链是现代存储系统(无论是本地、集中式还是分布式)的标准功能

带着问题去阅读:文件写入了一笔IO,为什么读取,却读不到?, 首先这个问题有歧义: 没有描述清楚:我写在哪里,怎么读的

开始

简单理解:快照有COW(Copy On Write,写时复制)和 ROW(Redirect On Write,写重定向)

快照有COW(Copy On Write,写时复制)

建议:直接放弃,看不见摸不着 ,出问题了根本无法跟踪定位

使用读多,写少情况,内存copy

写入性能相对较低:在写入时,需要复制发生变化的数据块【频繁写入场景 不建议使用,例如文件系统千万OPS】

举例 Redis的RDB快照正是利用了COW的思想来高效生成内存快照,避免了在fork()瞬间复制整个内存空间带来的巨大开销和停顿

Redis的设计哲学是简单与高效。fork()加COW是一种利用操作系统原生能力、实现相对简单且高效的快照方案【直接使用系统os提供能力,自己不去实现】

Redis不采用这种方式写重定向, ROW通常是存储系统(如SAN/NAS、分布式存储、文件系统)中针对磁盘数据块的快照技术。

ROW是底层存储系统用于实现磁盘卷多版本管理的空间优化型快照技术;

Redis RDB是内存数据库层面用于实现定期全量数据持久化的备份机制。

两者虽名含“快照”,但解决的问题域、实现原理和设计目标截然不同。

ROW的目标是为磁盘卷提供瞬间的、空间高效的、可回滚的时间点副本,其管理的是物理存储空间。它的核心操作对象是磁盘上的数据块

Redis是一个内存数据库,其首要设计目标是将内存中的复杂数据结构(字符串、哈希、列表等)持久化到磁盘文件

ROW(Redirect On Write,写重定向)

强烈推荐,就是新增代替修改 类似LSM 全量 +增量

ROW是底层存储系统用于实现磁盘卷多版本管理的空间优化型快照技术

源卷(Source Volume):生成快照的原始存储卷或数据集。快照通常是源卷的副本,用于保护和管理源数据。

目标卷(Target Volume):存储快照的位置,通常是一个单独的卷或存储空间。目标卷可以用于还原数据或创建新的副本。【新开辟一个空间】

快照链(Snapshot Chain):多个快照按照时间顺序形成的链式结构。每个快照记录了一个特定时刻的数据状态,并链接到前一个快照,以支持数据恢复和管理。

*只读快照:只读,不可变,用于数据保护和备份

可写快照:

克隆:可写,基于快照,可写快照

2.2 快照链 读流程(数据可能在磁盘上)

快照链的核心机制:

1、读取链:当前快照 → 父快照 → 父快照,直到找到数据或到达链顶端

2、写入隔离:通过 SnapContext 确保写入只影响当前版本,不影响已有快照

3、如果多个快照链 读变慢,元数据需要分离。

为什么链过长会影响性能?—— 寻址开销

这就是关键所在。我们把链想象成一个需要层层拆开的包裹。

链很短(比如只有2-3层):要找一个数据,最多回溯2-3步就能找到,很快。

链很长(比如几十层):要找一个数据(尤其是读取一个未被频繁修改的旧文件),系统需要从最新的增量盘开始,一层一层地回溯检查几十个文件,才能确定数据到底存放在哪一层。

一句话描述:

块存储 对用户来说,对同一位置LBA多次写入,在快照链机制下,可以创建快照保护历史写入数据不会修改,

同样原卷继续写入,创建的快照也继续写入,他们写入不同空间,很简单磁盘空间很大,不考虑重复浪费问题。

三、OceanBase 基于MVCC存储的IO 读写过程

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的核心运作机制,包括版本链的生成和读取过程:

下面我们来详细解读图中的关键环节和其背后的设计。

3.1 版本管理

版本链

1、在 MVCC 机制下,数据库中的每行数据都关联着一个版本链。

2、版本链记录了该行数据的多个版本,每个版本都对应着特定的事务操作。

事务时间戳

每当一个新的事务开始时,它会获得一个唯一的、递增的时间戳。 这个时间戳代表了事务的开始时间,并且将被用于确定事务之间的先后顺序

读-写不阻塞

在MVCC机制下,读操作不会阻塞写操作,写操作也不会阻塞读操作。 这是因为读操作读取的是数据的历史版本,而写操作创建的是数据的新版本,两者不会相互干扰。

1、读一致性:读操作只能看到快照版本之前已提交的数据

3.2 功能--数据结构

1. ObMvccEngine - MVCC引擎(封装功能,对外提供接口)

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_()触发行压缩

2. ObMvccRow - 多版本行

ObMvccRow是存储同一行键所有多版本数据的核心结构,维护一个从新到旧的双向链表。(ob_mvcc_row.h:228-232)

代码语言:javascript
复制
 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

版本链在内存中的组织
版本链在内存中的组织

版本链在内存中的组织

3. ObMvccTransNode - 事务节点

ObMvccTransNode代表一个事务对某行数据的单次修改,存储增量数据(仅包含被修改的列)。

代码语言:javascript
复制
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.

核心字段:

代码语言:javascript
复制
struct ObMvccTransNode 
{
	transaction::ObTransID tx_id_; // 事务唯一标识 
	share::SCN trans_version_; // 事务提交版本号 
	share::SCN scn_; // 序列号/日志时间戳 
	transaction::ObTxSEQ seq_no_; // 事务内序列号
}
4. ObMvccAccessCtx

读操作通过 ObMvccAccessCtx 携带快照信息 ObMvccAccessCtx 是 OceanBase 中 MVCC(多版本并发控制)的核心上下文对象

3.3 功能---读流程

读操作流程

1、使用 ObMvccAccessCtx 携带事务快照信息 ob_mvcc_acc_ctx.h:346-355

2、通过 ObMvccValueIterator 遍历事务节点链表

3、根据快照版本过滤可见数据

4、处理读锁冲突和事务集违反

假设有以下并发场景:

代码语言:javascript
复制
-- 事务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创建ObMvccAccessCtxsnapshot_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

3.4 功能--写流程

写操作流程

1、创建新的事务节点 (ObTxNodeArg)

2、调用 ObMvccRow::mvcc_write() 将节点插入链表头部 ob_mvcc_engine.h:80-89

3、处理写写冲突检测

4、通过 finish_kv() 使新版本对读操作可见

三、行压缩(Row Compaction)

压缩触发条件

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在冻结时才确定最终的快照版本,这确保了快照版本包含了所有已提交的事务

•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

五、MemTable到SSTable转储(Minor Compaction)

LSM-Tree架构中的层级

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类型的节点作为完整版本保存

可见性判断

读操作的可见性规则:

代码语言:javascript
复制
可见条件:  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

不是当前事务的未提交修改?

节点 N3(trans_version=10003):

10003 > 10002 → 不满足 trans_version <= snapshot_version

❌ 不可见,跳过

节点 N2(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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、对你有什么帮助
  • 二、依赖知识:文件系统快照 Snapshot基本特点
    • 2.1 了解基本系统设计原理
  • 三、OceanBase 基于MVCC存储的IO 读写过程
    • 3.1 版本管理
    • 3.2 功能--数据结构
      • 3. ObMvccTransNode - 事务节点
    • 3.3 功能---读流程
      • 读操作流程
    • 3.4 功能--写流程
      • 写操作流程
    • 三、行压缩(Row Compaction)
      • 压缩触发条件
      • 压缩过程
    • 四、MemTable冻结过程
      • 冻结触发
      • 冻结状态转换
      • 冻结后的处理
    • 五、MemTable到SSTable转储(Minor Compaction)
      • LSM-Tree架构中的层级
      • 转储流程 [内存--磁盘]
      • 多版本数据处理
      • 可见性判断
      • 版本隔离保证
    • 🧪 举例说明:事务修改与版本链构建
      • 场景描述:
      • ❓ 查询可见性分析
    • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档