0
0
1
0
博客/.../

为什么 TiKV 要把事务数据拆成三个"抽屉"?

 TiDBer_wangwenjing  发表于  2026-09-07

引子:培训课上一个让人费解的概念

前一阵子参加 TiDB 的培训,在介绍事务实现时,讲师提到了一个关键概念——Column Family(CF)。

为了帮助大家理解,讲师把 CF 比喻成一个"分区",更准确一点应该是“逻辑分区-独立的存储区域”。

作为一个长期和数据库打交道的人,脑子里马上冒出了几个问题:

  • CF 到底是什么?和 HBase 里的 CF 是同一个东西吗?
  • 为什么 TiKV 里面会出现 default、lock、write 三个 CF?
  • 数据为什么不能简单放在一起?

带着这些疑问,我重新翻了一下 Google 的 Bigtable、Percolator 两篇经典论文,又顺着 TiKV 和 RocksDB 的实现一路往下深入。

最后发现:

Column Family 这个名字一直没有变,但它在不同系统里的职责发生了变化。

在 Bigtable 中,它是连接数据模型和存储管理的一种组织方式;

到了 HBase,它成为用户可以直接设计的数据模型;

而到了 TiKV,它借助 RocksDB 的 Column Family 机制,成为存储引擎内部隔离不同类型数据的工具。

还是那个“抽屉”,只是以前抽屉是用户整理数据的工具,到了 TiKV,它成了存储引擎自己管理数据的工具。

不过,在深入之前,需要先澄清一个很多人都会产生的误解:

TiKV 并不是把一张用户表直接拆成三个 Column Family。

真正发生的是:

TiDB 先把表编码成 Key-Value,再按照 Percolator 的事务模型,把不同类型的数据分别写入 default、lock、write 三个 CF。

也就是说,拆分发生在事务存储层,而不是用户表层。

理解这一点,后面很多设计就顺理成章了。

一、追根溯源:Bigtable 为什么提出 Column Family?

要理解 Column Family,需要回到 2006 年 Google 发布的经典论文:

Bigtable: A Distributed Storage System for Structured Data

在 Bigtable 中,一条数据由四个维度共同定位:

Row Key + Column Family + Column Qualifier + Timestamp

其中:

  • Row Key:行键
  • Column Family:列族
  • Column Qualifier:列名
  • Timestamp:版本号

很多人第一次看到 Column Family,都容易把它理解成"列的文件夹"。

其实,它更准确的定位是:

Column Family 是一组列的命名空间(Namespace)。真正的列由「Column Family + Column Qualifier」共同确定。

例如:

Row Key

Column Family

Column Qualifier

user_001

info

name

user_001

info

age

user_001

address

country

user_001

address

city

这里,info 和 address 就是两个 Column Family,而 nameagecountrycity 则是对应的 Column Qualifier。

也就是说,真正的列名其实是:

  • info:name
  • info:age
  • address:country
  • address:city

那么,Bigtable 为什么没有直接使用普通列,而是引入 Column Family?

原因主要有三个。

1. 逻辑分组

同一个 Column Family 中的列,通常具有相似的数据类型和访问模式。

例如,用户基本信息(info:nameinfo:age)和用户地址信息(address:countryaddress:city)天然属于不同类别。

2. 管理单元

Column Family 同时也是资源管理的基本单位。

例如:

  • 访问控制(ACL);
  • 内存统计;
  • 磁盘统计;
  • 压缩策略。

也就是说,它不仅是一个逻辑概念,也在告诉存储系统:这些数据应该作为一个整体进行管理。

3. 数据局部性

不同类型的数据,访问模式往往完全不同。

例如,用户资料(user_profile)经常被扫描,而用户日志(user_log)更多是持续追加写入。

如果把两类数据混在一起:

  • 扫描会读入大量无关数据;
  • 缓存容易互相挤占;
  • 压缩策略也难以兼顾。

因此,Bigtable 提出了 Column Family,让不同类型的数据拥有不同的存储策略。

这也是 Column Family 最初的设计初衷:把逻辑上的数据组织和物理上的存储管理结合起来。

二、HBase:Bigtable 的开源实现

提到 Column Family,大多数人首先想到 HBase。

原因很简单:HBase 几乎完整继承了 Bigtable 的设计理念。

在 HBase 中:

  • 建表时需要定义 Column Family;
  • 每个 CF 对应独立的 Store;
  • 每个 Store 管理自己的 StoreFile;
  • 每个 CF 可以配置不同的 TTL、压缩算法、Bloom Filter 等参数。

需要注意的是,Column Family 是固定的,但是 Family 下有哪些列(Qualifier),可以动态增加。

例如,info:nameinfo:ageinfo:email 可以在写入时直接新增。

不过,这种设计也不是没有代价。

每增加一个 Column Family,就意味着需要维护更多:

  • MemStore;
  • StoreFile;
  • Flush;
  • Compaction。

因此,HBase 社区一直建议:不要为一张表创建过多 Column Family。

直到这里,Column Family 仍然属于用户的数据模型。

三、到了 TiKV,Column Family 的角色变了

TiKV 同样使用了 Column Family。

但这里的 CF,已经不是 HBase 那个意义上的 CF。

特性

HBase

TiKV

用户是否可见

定义方式

建表时定义

存储引擎内部创建

用途

组织用户数据

隔离不同类型的数据

一句话总结就是:

HBase 的 Column Family 面向数据建模;TiKV 的 Column Family 面向存储实现。

不过,它们仍然保留了 Bigtable 最重要的一点:每个 Column Family 都是一个独立的数据存储单元。

到了 RocksDB,这句话可以进一步具体化。

在 RocksDB 的实现中,一个 Column Family 基本可以看成一棵独立的 LSM Tree。

为什么?

因为每个 CF 都维护着自己的一整套状态:

  • MemTable;
  • Immutable MemTable;
  • SST 文件;
  • Version;
  • Compaction。

而多个 CF 则共享同一个 RocksDB 实例,例如:

  • WAL(Write-Ahead Log);
  • 后台线程池;
  • (可选共享的)Block Cache。

因此,TiKV 的三个 CF,并不是三个数据库,而是同一个 RocksDB 实例中的三棵 LSM Tree。

四、为什么 TiKV 要设计三个 CF?

终于来到本文最核心的问题。

为什么不是一个 CF,而是 default、lock、write 三个?

答案来自 Google 的另一篇经典论文:

Percolator: Large-scale Incremental Processing Using Distributed Transactions and Notifications

需要注意的是:Percolator 并没有定义三个 Column Family,它定义的是三类逻辑数据:

  • data
  • lock
  • write

TiKV 借助 RocksDB 的 Column Family,把这三类数据映射到了三个独立的 CF 中。

Percolator 逻辑数据

TiKV CF

存储内容

data default

真正的业务数据

lock lock

事务锁信息

write write

提交记录

所以,defaultlockwrite 并不是 Percolator 的设计,而是 TiKV 在 RocksDB 上的一种工程实现。

为什么不能放在一起?

因为它们的数据特征完全不同。

CF

数据大小

生命周期(访问特点)

lock

极小

毫秒级(延迟极其敏感)

write

小(几十字节)

较长(高频写入、高频查询)

default

可达 KB~MB

最长(保存真实业务数据)

对于存储引擎来说,有一个非常重要的原则:

生命周期相近、访问模式相似的数据,尽量放在同一棵 LSM Tree 中。

如果三类数据混在一起:

  • 生命周期不同的数据会频繁参与同一次 Compaction,造成大量无意义的数据重写,增加写入放大(Write Amplification);
  • 大 Value 的整理会拖慢小记录的 Compaction;
  • 大对象会不断挤占 Block Cache,影响热点小数据的访问效率。

因此,TiKV 选择把它们拆分到三个独立的 Column Family。

最终,一个 RocksDB 实例内部,同时维护着三棵独立的 LSM Tree:

  • default CF:保存真正的数据;
  • write CF:保存版本记录;
  • lock CF:保存事务锁。

每棵树都有自己的 MemTable、SST 文件、Version 和 Compaction 逻辑。

它们在 LSM 管理层面彼此独立,因此:

  • default 中的大对象整理,不会拖慢 lock 的访问;
  • write 中高频的小记录写入,也不会增加 default 的 Compaction 负担。

当然,它们仍然共享 WAL、线程池等公共资源,因此这里说的"隔离",主要指的是 LSM Tree 层面的隔离。

五、从源码看:为什么说 CF 就是一棵 LSM Tree?

如果继续往 RocksDB 源码里追,很快就会遇到一个核心对象:

ColumnFamilyData

它并不仅仅保存一个 CF 的名字。

实际上,它负责维护一个 Column Family 从创建到运行的整个生命周期,其中包括:

  • MemTable;
  • Immutable MemTable;
  • Version;
  • SST 文件;
  • Compaction 状态。

也就是说,在 RocksDB 内部,并不是"一个 LSM Tree 下面分三个目录",而是:

一个 RocksDB 实例下面,维护着多个彼此独立的 LSM Tree。

这也是为什么很多 RocksDB 开发者都会直接说:

一个 Column Family,就是一棵独立的 LSM Tree。

从这个角度再回头看 TiKV 的设计,也就不难理解了:

创建一个新的 CF,本质上就是在同一个 RocksDB 实例中新增一棵 LSM Tree。相比启动一个新的数据库实例,它成本更低;相比把所有数据混在一起,它又提供了足够好的物理隔离能力。

这正是 TiKV 选择这种实现方式的重要原因。

六、Column Family 的角色,一直在演进

回顾整个发展过程,可以发现:

  • Bigtable 提出了 Column Family,把数据组织和物理存储管理结合起来;
  • HBase 继承了 Bigtable,把 Column Family 变成了用户可见的数据模型;
  • TiKV 借助 RocksDB 的 Column Family 机制,将 Column Family 用作存储引擎内部的数据隔离单元,用来承载 Percolator 的 data、lock、write 三类逻辑数据。。

核心思想没有变,但服务对象发生了变化。。

Bigtable、HBase 的 Column Family,解决的是数据如何组织的问题;TiKV 的 Column Family,解决的是数据如何高效存储的问题。

结语:三个抽屉,其实是三棵 LSM Tree

现在再回头那句话:

"Column Family 就是一个独立的存储区域。"

其实没有说错,只是少说了一半。对于 TiKV 来说,更准确的说法应该是:

一个 Column Family,不只是一个独立的存储区域,更是一棵独立管理的 LSM Tree。

  • default:保存真实业务数据;
  • write:保存版本信息;
  • lock:保存事务锁。

三者各司其职,又共同支撑起了 TiDB 的 MVCC 和分布式事务。所以,下次再看到 TiKV 的三个 CF 时,不妨把它们想象成三个分工明确的"抽屉"。不过,它们装的不只是不同类型的数据——它们背后,其实是三棵彼此独立、又协同工作的 LSM Tree。

0
0
1
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论