列族(Column Families)是rocksdb3.0提出的一个机制,用于对同一个数据库的记录(键值对)进行逻辑划分。默认情况下所有的记录都会存储在一个默认列族里:
ROCKSDB_NAMESPACE::kDefaultColumnFamilyName
列族具有的属性
1)可以跨列族进行原子写,弥补了rocksdb在单个进程内只能操作一个数据库的问题。
2)在不同的列族,提供数据库的一致性视图。
3)可以对列族进行独立配置。
4)动态添加和drop列族。
ColumnFamilyOptions用于配置列族;
DBOptions用于数据库粒度的配置;
Options继承了ColumnFamilyOptions, DBOptions,因此Options可以执行上述两种配置。
每个列族通过句柄类ColumnFamilyHandle进行操作,包括列族的创建和销毁都要使用ColumnFamilyHandle完成。
创建列族,下列代码就创建了一个名为"new_cf"的列族:
ColumnFamilyHandle *handl;
db->CreateColumnFamily(ColumnFamilyOptions(), "new_cf", &handl);
db->DestroyColumnFamilyHandle(handl);
使用列族,基于列族打开数据库,和常规的方式不同。具体来说,如果我们以读写模式打开数据库,则必须要给open传入所有的列族,否则open方法会返回Status::InvalidArgument()。
每个列族通过ColumnFamilyDescriptor表示,这个类包含了{列族名,ColumnFamilyOptions}。
下列代码通过读写模式打开了一个包含所有列族的数据库:
vector colume_families;
colume_families.push_back(ColumnFamilyDescriptor(ROCKSDB_NAMESPACE::kDefaultColumnFamilyName, ColumnFamilyOptions()));
colume_families.push_back(ColumnFamilyDescriptor("new_cf"), ColumnFamilyOptions())
vector cf_handles;
Status s = db->open(DBOptions(), DB_PATH, colume_families, &cf_handles, &db);
如果使用只读模式打开数据库,那么可以只传入我们需要读取的列族,不过默认的列族是必须要传入给open函数的
// open db read only
column_families.push_back(ColumnFamilyDescriptor("new_cf", ColumnFamilyOptions()));
column_families.push_back(ColumnFamilyDescriptor(kDefaultColumnFamilyName, ColumnFamilyOptions()));
s = DB::OpenForReadOnly(DBOptions(), kDBPath, column_families, &handles, &db);
assert(s.ok());
s = db->Get(ReadOptions(), handles[0], Slice("key2"), &value1);
assert(s.ok());
通过WriteBatch我们可以原子的操作不同的列族,例如可以通过handles[0]去删除handles[1]插入的键值对{"key": "value"}
// put and get from non-default column family
s = db->Put(WriteOptions(), handles[1], Slice("key"), Slice("value"));
assert(s.ok());
std::string value;
s = db->Get(ReadOptions(), handles[1], Slice("key"), &value);
assert(s.ok());
// atomic write
WriteBatch batch;
batch.Put(handles[0], Slice("key2"), Slice("value2"));
batch.Put(handles[1], Slice("key3"), Slice("value3"));
batch.Delete(handles[0], Slice("key"));
s = db->Write(WriteOptions(), &batch);
assert(s.ok());
4) 原理和实现
简单的说,不同的列族是共享WAL的,但是memtable和SST file是隔离的。
rocksdb基本可以认为就是对LSM Tree的实现,因此LSM Tree对于rocksdb是非常重要的。
LSM Tree的设计可以参考论文:
1996, The Log-Structured Merge-Tree (LSM-Tree)
2014, A Comparison of Fractal Trees to Log-Structured Merge (LSM) Trees
2017, WiscKey: Separating Keys from Values in SSD-conscious Storage, TOS
2019, LSM-based Storage Techniques: A Survey
数据的存储方式,可以分为两种:in-place update和out-of-place update,LSM Tree是out-of-place update的方式,而in-place update的典型代表是B+树。
in-place update的方式,进行数据的写入/修改/删除操作时,是会对硬盘上的已有数据进行修改的,例如写入新数据时,为了维护B+树的结构,可能需要进行节点的分裂,另外,为了维护B+树的结构,写入的数据可能分布在不同的位置,会有比较多的随机io。
out-of-place update的意思,就是说进行数据的写入/修改/删除操作时,并不对硬盘上的已有数据进行修改,而是先在内存中进行记录(写入/修改就记录数据内容,删除就记录需要删除的key),在达到一定的数据量后,生成一个新的文件,所以硬盘中可能同时存在新的和旧的数据(一般kv存储要求key是唯一的,新写入key0 value1后,旧的key0 value0应当失效,删除掉旧的数据,或者使其无法被读取出来)。为了避免占用的硬盘空间不停上升,以及存在重复数据导致的搜索效率下降,需要定期/不定期的处理重复数据,LSM Tree的处理方法是执行compaction,即选择一些文件进行合并,删除重复的数据后,生成新的不包含重复内容的文件。由于写入和compaction时,写入的都是完整的文件,所以随机io很少,都HDD和SSD来说是比较友好的。但是由于存在compaction操作,一个数据可能被重复读写多次,所以会造成执行io的数据量比实际写入的数据量要大几倍甚至几十倍,也就是写放大问题。
compaction是LSM Tree进行数据维护的核心工作,compaction的执行主要分为两种策略:Leveling Merge Policy和Tiering Merge Policy。
参考链接:https://blog.csdn.net/xuhaitao2