首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深入理解MVCC: LSM-Tree IO读写不支持多版本并发控制 ,如何改造IO读写支持MVCC

深入理解MVCC: LSM-Tree IO读写不支持多版本并发控制 ,如何改造IO读写支持MVCC

作者头像
早起的鸟儿有虫吃
发布2026-01-14 15:38:20
发布2026-01-14 15:38:20
1250
举报

一、对你有什么帮助

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

本文聚焦:TiKV 数据库的 MVCC(多版本并发控制)机制 通过巧妙的 key 编码和多列族架构实现了完整的 MVCC 支持。 大纲

原生LSM-Tree IO读写过程

为了支持MVCC ,不修改LSM-Tree底层结构基础上,需要设计那些新的数据结构?

基于MVCC机制下IO流程优化?

二、最少依赖知识:快照链读写特点

大家都都知道

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

内存特点就是:速度快,容量少 ,

Redis采用 fork()加Copy On Write是一种利用操作系统原生能力、实现相对简单且高效的快照方案

假如文件系统 磁盘 想采用提供方式用于实现磁盘卷多版本管理的空间优化型快照技术 肯定不不会这样设计,

哪怕采用全闪磁盘 特点也是 空间容量大,

存储系统采用虚拟化存储技术。 存储池中创建的LUN由元数据卷(Meta Volume)和数据卷(Data Volume)两部分组成

快照生成并激活后,存储系统在源LUN所在的存储池中动态划分一部分存储空间,用于保存写前拷贝数据。同一个源LUN对应的所有快照LUN共享同一个COW数据空间。COW数据空间包括COW Meta区域和COW Data区域:

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

读操作路径:从最新快照向最旧快照回溯

三、正式开始 探索MVCC之旅

3.1 原生LSM-Tree IO读写过程

参考 302-TiDB 高级系统管理 内容

旁白:不考虑 MVCC ,rockdb raft transaction部分内容,只关注rocksdb 本身

IO 写入过程

IO读取过程

CF三连问(1):Column Family (CF)基本原理是什么?

所有列族共享同一个预写日志(WAL),这确保了跨多个列族的写入操作可以具有原子性(要么全部成功,要么全部失败)。

每个列族都拥有自己独立的MemTable(内存表)、Immutable MemTable和SST文件

CF三连问(2):这样设计有什么意义

清晰的数据组织:开发者可以将不同类型、不同业务或不同生命周期的数据存入不同的CF

通过CF,TiKV成功地将元数据、主体数据、临时状态数据(锁)和内部协调数据进行了物理分离和逻辑统一管理,符合现实设计意义

高效的独立操作:由于数据物理分离,对某个CF的增、删、改、查以及Compaction(合并)操作,基本不会影响其他CF的性能

CF三连问(3):还是不明白,举例说明

第一次去 首都图书馆占地面积3.8万平方米,百万册图书, 简直是刘姥姥进大观园,找一本太难了,假如你是馆长 不会把所有东西都堆在一个房间里,而是分成了四个功能不同的房间, 每个房间就是一个“列族”。

下面我们来“参观”一下这个图书馆的四个房间:

列族 (CF)

图书馆中的角色

具体职责与特点

write (写)

图书目录卡

这是图书馆的检索核心。它不存整本书,只记录每本书的关键信息:书名(Key)、入库时间(MVCC的开始和提交时间戳)、以及这本书在哪个书架(如果书很薄,甚至会把整本书内容抄在卡片背面)。在TiKV中,它存储真实的写入数据和MVCC信息,如果数据小于255字节,就直接存在这里

default (默认)

藏书库

这里是存放实体书的地方。当一本书太厚(数据大于255字节),目录卡(write)上写不下全部内容,就会在这里存一份完整的副本,并在目录卡上备注“藏书库,A区3排2架”。这样,目录卡就能保持轻便,查询更快

lock (锁)

借阅登记处

这是一个临时工作台。当有人要修改某本书时,需要在这里登记“此书正在修订中,暂不外借”。这个记录是临时的,一旦修改完成(事务提交),记录就会立刻被擦除。如果这里排了长队,说明系统可能出问题了

raft (raft)

馆长办公室

这个房间很小,只存放图书馆的内部管理文件,比如各个区域的分区图、值班表等。普通读者(用户)完全不用关心这里,它只对图书馆管理员(TiKV自身)有用

所以,TiKV创建这四个列族,根本目的是对数据进行逻辑上的“分房间管理”

write:存核心索引和小数据,追求最快的查询速度。

default:存大数据本体,是write的“仓库”。

lock:存临时的事务锁,生命周期短。

raft:存内部元数据,体量极小。

当读者(事务)想找某本书在2024年(read_ts)的最新版本时, 他只需去索引卡片柜(write列族),找到该书在2024年之前最新的出版记录卡片, 然后根据卡片指引,去藏书库找到对应的书籍。

它通过集中管理所有数据的版本元信息(时间戳)和访问路径,使得系统能够以O(log N)的复杂度,快速定位到任意Key在任意时间点下的正确数据版本,从而高效地实现了快照隔离级别

具体存储如下:

3.2 为了支持MVCC ,不修改LSM-Tree底层结构基础上,需要设计那些新的数据结构?

TiKV的MVCC(多版本并发控制)机制与底层LSM-Tree(RocksDB)的结合,并非简单的叠加

而是通过一系列精妙的设计与改进,实现了高性能、高并发的分布式事务处理。

其核心改进在于将MVCC版本信息内嵌到Key中,并利用LSM-Tree的特性进行高效管理

TiKV使用 RocksDB 作为底层存储引擎没有改变

LSM-tree 结构具有以下特点:

1、分层存储:数据在内存中的 MemTable 和磁盘上的 SST files 之间分层存储

2、有序排列:Key 按字典序排列,版本号较大的排在前面

3、持久化:所有数据最终持久化到磁盘,不是纯内存存储

核心改造机制

1. 最根本的改进 时间戳编码的 Key 设计

Key三连问(1) 时间戳编码的 Key 设计是什么?

TiKV 通过在用户 key 后追加时间戳来实现版本化存储

这种设计利用了 LSM-Tree 的有序性,使得版本号较大的 key 排在前面,便于版本查找。

代码语言:javascript
复制
// 构造带时间戳的 key  let k = key.clone().append_ts(start_ts);  let val = self.snapshot.get(&k)?;
Key三连问(2) 为什么这样设计,而不是直接向ob那样直接修改 lsm table结构,去直接改造了RocksDB本身?

TiKV选择将时间戳编码到Key中,而不是像OceanBase那样直接改造LSM-Tree(RocksDB)的内部结构,是一个深思熟虑的架构权衡。

1、保持兼容性和可维护性

上游兼容: 避免维护自定义 RocksDB 分支,减少维护成本

•版本升级: 可以跟随 RocksDB 官方版本升级,获得性能改进和 bug 修复

•社区支持: 享受 RocksDB 社区的生态支持和优化

相反:直接改造,懂c++不多呀,尤其是研究生 都搞rust去了。

1、极高的工程复杂度和维护成本:需要深入改动一个像RocksDB这样复杂的存储引擎的每一层,包括WAL、MemTable、SSTable格式、Bloom Filter、索引块、Compaction算法等。任何改动都可能引入难以预料的稳定性问题,且需要团队具备极其深厚的存储内核研发能力。

2、与上游社区脱节:改造后的存储引擎将成为一个分支,难以持续、平滑地合并上游RocksDB社区的优化和修复,需要投入巨大精力进行二次维护和融合。

将时间戳编码到Key中的外部编码”方案,其核心优势可高度概括为以下三点,

1、实现高效的历史数据查询:由于LSM-Tree(及同类存储如HBase)的数据在物理上按键的字典序有序存储,将时间戳作为Key后缀,使得同一个逻辑Key的所有历史版本在磁盘上连续排列。这天然地将“按时间点查询特定版本”或“按时间范围扫描”的复杂逻辑,转化为了在有序序列上的高效Seek或范围扫描操作,极大提升了基于时间戳的快照读和范围查询性能。

2、为分布式事务提供原子性基石:该设计完美契合了类似Google Percolator的分布式事务模型。在此模型中,全局唯一、单调递增的时间戳是协调事务状态(开始、提交、回滚)的核心。将start_tscommit_ts编码到不同数据(如write列族)和锁(lock列族)的Key中,使得跨多行、多表的事务状态变更,可以通过对一组带有特定时间戳的Key进行原子操作来实现,这是构建跨节点一致性的直接且清晰的方式。

3、简化多版本数据的生命周期管理:MVCC机制会产生大量过期数据版本。由于所有版本按时间戳有序存储,垃圾回收(GC)机制变得非常简单直接

3.3 为了支持MVCC ,分布式事务如何存储的

TiKV 采用了 [Google Percolator] 这篇论文中所述的事务模型

https://tikv.org/deep-dive/distributed-transaction/percolator/

Percolator is built based on Google’s BigTable, a distributed storage system that supports single-row transactions. Percolator implements distributed transactions in ACID snapshot-isolation semantics, which is not supported by BigTable. A column c of Percolator is actually divided into the following internal columns of BigTable:

c:lock

c:write

c:data

c:notify

c:ack_O

分布式事物三连问(1) Google Percolato 主要内容是什么

Percolator在BigTable中,并非直接将用户数据存入一个简单的键值对。 为了实现事务,它为每一行用户数据引入了三个特殊的“元数据列”(在TiKV中对应为lock, write, default 列族)

data:存储事务写入的实际数据值。其Key的格式为 {用户row, 用户column, start_ts},Value是用户数据。这创建了一个由start_ts标识的、尚未提交的数据版本

write:存储提交记录,标志着某个数据版本已成功提交。其Key的格式为 {用户row, 用户column, commit_ts},Value是对应的start_ts。通过查询write列,可以找到在某个时间点(commit_ts)已提交的最新数据版本指向哪个data记录

lock:存储进行中事务的锁。其Key为 {用户row, 用户column},Value包含锁持有者的信息(如primary lock的位置)。用于在事务提交过程中防止其他事务干扰

start_tscommit_ts作为Key的一部分,分别编码到datawrite列中。

乐观事务模型
乐观事务模型

乐观事务模型

start_tscommit_ts作为Key的一部分,分别编码到datawrite列中。

分布式事物三连问(2) 分布式事物如何存储到KVDB中。
请看 302-TiDB 高级系统管理
请看 302-TiDB 高级系统管理

请看 302-TiDB 高级系统管理

你会发现

Default:update 3 =frank,记录最新修改内容,至于3以前内容不记录

记录lock区域:begin 加锁操作

记录lock区域:commit 提交 有增加一个D 记录

优化 :小文件存储,小于255字节直接存储到wirte 索引区域

数据存储到多个节点怎么加锁,主锁 和非主锁

分布式事物三连问(3) 这个存储方式对IO读写有什么影响?

写 id=1,不影响读

写写互斥

4. 基于MVCC机制下IO流程优化

TiKV的MVCC实现受到Google Percolator论文的启发

三列族设计分离了数据、元数据和锁,优化了读写性能

时间戳由PD(Placement Driver)全局分配,确保事务顺序

支持乐观锁和悲观锁两种事务模式

TiKV 通过三个列族(Column Family)来存储MVCC数据 txn.rs:163-179 :

列族

用途

存储内容

CF_DEFAULT

存储实际数据值

key + timestamp → value

CF_WRITE

存储事务元数据

key + commit_ts → Write(start_ts, type)

CF_LOCK

存储活跃事务锁

key → Lock(start_ts, primary, ttl)

RocksDB 作为 TiKV 的核心存储引擎,用于存储 Raft 日志以及用户数据。 每个 TiKV 实例中有两个 RocksDB 实例,一个用于存储 Raft 日志(通常被称为 raftdb), 另一个用于存储用户数据以及 MVCC 信息(通常被称为 kvdb)。

kvdb 中有四个 ColumnFamily:raft、lock、default 和 write:

raft 列:用于存储各个 Region 的元信息。仅占极少量空间,用户可以不必关注。

lock 列:用于存储悲观事务的悲观锁以及分布式事务的一阶段 Prewrite 锁。当用户的事务提交之后,lock cf 中对应的数据会很快删除掉,因此大部分情况下 lock cf 中的数据也很少(少于 1GB)。如果 lock cf 中的数据大量增加,说明有大量事务等待提交,系统出现了 bug 或者故障。

write 列:用于存储用户真实的写入数据以及 MVCC 信息(该数据所属事务的开始时间以及提交时间)。当用户写入了一行数据时,如果该行数据长度小于或等于 255 字节,那么会被存储 write 列中,否则该行数据会被存入到 default 列中。由于 TiDB 的非 unique 索引存储的 value 为空,unique 索引存储的 value 为主键索引,因此二级索引只会占用 writecf 的空间。

default 列:用于存储超过 255 字节长度的数据。

读放大问题如何解决

TiDB 的 MVCC 多版本数据存储实现机制,在 Key 上会标识数据版本。

-- TiDB 数据存储结构示例

Key: table_id{row_id}_timestamp

Value: column_data + transaction_info

-- 实际存储格式:

-- Key: t{123}_r1_v5 (table 123, row 1, version 5)

-- Value: {name: "John", age: 25, start_ts: 5, commit_ts: 10}

TiDB的MVCC机制通过在Key中编码时间戳(版本号)来实现快照隔离和并发控制, 这是一种经典且强大的设计

由于RocksDB(TiKV的底层存储引擎)按Key的字典序存储数据,同一行数据(t{123}_r1)的所有历史版本(v1, v2, v3...)会在物理磁盘上连续排列。[读磁盘就是慢 这个问题本身无法解决]

问题场景举例: 假设表123中行r1被频繁更新了1000次,那么磁盘上就会存在 t123_r1_v1t123_r1_v1000 这1000个Key。

当执行一个需要读取最新数据的查询(例如 SELECT * FROM table WHERE id=1)时,TiKV的读取逻辑(MvccReader)需要:

1、write CF 中,定位到 t123_r1 这个前缀。

2、向后扫描(Seek)所有以 t123_r1 开头的Key,直到找到小于或等于当前事务快照时间戳(start_ts)的最大版本号对应的记录。

3、、如果这个最新版本是一个Rollback记录或已被删除,则需要继续向前扫描寻找上一个有效版本。

这个过程意味着,即使你只想读取一行数据的最新状态,存储引擎也可能需要实际扫描该行的数十甚至数百个历史版本。对于范围查询(如 SELECT * FROM table WHERE id BETWEEN 1 AND 1000),这个问题会被指数级放大、

TiKV的优化:正是为了应对这一问题,TiKV在v8.5.0引入了 MVCC内存引擎(In-Memory Engine, IME)

IME的核心思想是将最新的数据版本缓存在内存中。 当进行扫描时,系统优先从内存中查找数据,如果可以命中, 则能完全避免在磁盘上遍历大量历史版本, 从而大幅提升扫描性能。

TiKV MVCC 内存引擎 (In-Memory Engine, IME) 主要用于加速需要扫描大量 MVCC 历史版本的查询

IO写过程

写入路径:MVCC的写入在TiKV层面变成了带时间戳的新Key的插入。这完美契合了LSM-Tree将随机写转换为顺序写的核心优势。写操作只需追加写入WAL和MemTable,性能很高。

Phase 1: Prewrite阶段
Prewrite命令处理

Prewrite命令负责第一阶段的数据写入 prewrite.rs:495-558 :

1、创建MVCC事务:使用start_ts初始化MvccTxn prewrite.rs:539-543

2、冲突检测:检查写冲突和锁冲突

3、写入数据:将数据写入CF_DEFAULT,锁写入CF_LOCK

Phase 2: Commit阶段

Commit命令处理

Commit命令负责第二阶段的提交 commit.rs:52-97 :

1、验证时间戳:确保commit_ts > lock_ts commit.rs:54-59

2、提交每个key:遍历所有key执行提交操作

3、生成WriteResult:返回提交结果

总结

TiKV的MVCC机制并非在LSM-Tree之上做简单封装,而是通过 “编码内化、数据分离、异步回收” 三大核心设计,与LSM-Tree深度集成:

1、将版本号编码进Key,利用LSM-Tree有序存储特性实现高效版本检索。

2、利用Column Family分离数据职责,优化访问模式与缓存效率,适配事务状态管理。

3、构建基于时间戳的异步垃圾回收,与LSM-Tree的Compaction机制协同,控制存储成本。

这些改进使得TiKV在继承LSM-Tree高写入吞吐、高压缩效率优点的同时,具备了处理分布式事务、支持快照隔离级别的能力,从而成为TiDB HTAP架构的坚实存储基石。

。其核心改进在于将MVCC版本信息内嵌到Key中,并利用LSM-Tree的特性进行高效管理,从而在保持LSM-Tree高写入吞吐的同时,支持了复杂的快照读和事务隔离

参考资料

https://deepwiki.com/tikv/tikv

https://tidb.net/blog/614b0fe3

https://pingkai.cn/docs/tidb/stable/tidb-storage/#mvcc

https://github.com/feitian124/tidb-course-302

302-TiDB 高级系统管理

https://learn.pingcap.cn/learner/course/1290019

Talent Plan 之 TinyKV 学习推荐课程

https://learn.pingcap.cn/learner/course/390002

[3]TiKV 源码解析系列文章(十三)MVCC 数据读取

[4]TiKV 源码解析系列文章(十二)分布式事务

TiKV 的 MVCC(Multi-Version Concurrency Contro) https://tidb.net/blog/e51d71b2

https://tidb.net/blog/8adf5d7d# TiKV MVCC 内存引擎 In-Memory Engine 实现数据多版本场景性能优化

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、对你有什么帮助
  • 二、最少依赖知识:快照链读写特点
  • 三、正式开始 探索MVCC之旅
    • 3.1 原生LSM-Tree IO读写过程
    • 3.2 为了支持MVCC ,不修改LSM-Tree底层结构基础上,需要设计那些新的数据结构?
      • 核心改造机制
      • 1. 最根本的改进 时间戳编码的 Key 设计
      • 3.3 为了支持MVCC ,分布式事务如何存储的
      • 4. 基于MVCC机制下IO流程优化
      • 读放大问题如何解决
      • Commit命令处理
      • 总结
    • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档