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

一、对你有什么帮助

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

  • 本文聚焦: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 排在前面,便于版本查找。
// 构造带时间戳的 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] 这篇论文中所述的事务模型

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中。

你会发现

  • 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 + timestampvalue
CF_WRITE 存储事务元数据 key + commit_tsWrite(start_ts, type)
CF_LOCK 存储活跃事务锁 keyLock(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高写入吞吐的同时,支持了复杂的快照读和事务隔离

参考资料

祝:

下定决心:

努力不挣钱没关系,

关键不要赔上百万,千万,

熬夜看手机就是 对眼睛,无法恢复的伤害。

一个亿也无法挽回。

手碰一下手机,耳朵听手机声音,看手机屏幕,还是看消息内容

陷入这样 虚拟世界,

无论现实遇到什么问题,哪怕活不到好工作,好项目,好机会

哪怕什么都不懂,努力 0收入 赔上百万,千万都不重要。

都不超过1个亿,最后还是赚了。

2026 重启手机,重启人生

对你操作系统赋予新意义开启

不要独自一个人看手机,

我们常常陷入这样的场景:
独自一人时,在餐厅、地铁、卧室、沙发或书桌前,
当你躺在那里,趴在哪里,做在哪里时候,身体固定狭小空间,无法互动 ,不自觉地掏出手机。
身体被困在狭小的物理空间里,无法动弹,
只能目光便只能被那方寸屏幕牢牢吸引,
你行为被 多巴胺诱惑,简单舒服即使反馈奖励 ,被平台设计各种陷阱控制

除非拥有极强的意志力,根本不选择痛苦迟到的奖励

与其对抗本能,甚至平台 不如改变环境。
请选择去户外,去操场,视眼开阔 看手机。
请主动为你的手机使用选择更健康的场景

核心行动准则1:为特定场景设立无手机时间

  • 进入公司开始工作时
  • 下班回到家中时
  • 在餐厅用餐或社交时
  • 乘坐地铁通勤时

行动建议:

  • 在上述场景开始时,立刻将手机放入书包或固定在某个位置(如抽屉)。
  • 给自己设定一个专注时限,例如至少接下来的3小时内不主动查看
  • 这能有效打破“无聊就刷手机”的循环,把注意力还给当下的人和事。

核心行动准则2:换个开阔的地方看手机

  • 早晨起床后
  • 下班之后
  • 周末时光
    行动建议:
  • 可以选择去图书馆、咖啡馆、商场中庭或景点休息区,公司园区,马路边
  • 在这些具有公共生活感的场所使用手机,
  • 周围的环境流动能天然地分散你对屏幕的过度专注,避免陷入无休止的刷屏。

一句话描述:

普通人最简单方式,重启自己操作系统

  • 固定21点入睡:1 R90睡眠方案之所以能这样的世界顶尖运动员所青睐,每天晚上的睡眠规律你可以
  • 固定6点起床:2 成不了作家 你可以打开笔记本写一行文字,3 做不出产品产品你打开软件写一行代码,4 无法演讲信服的话,你自己说一句话。5 成不运动健身达人 你走到运动走一步

今日记录

第11/30天:晚上21点以后不打开电脑控制不了

远离手机:每天5个小时 明天降低30分钟

1 个赞