diff --git a/CMakeLists.txt b/CMakeLists.txt index 7307c47..8fd8ce7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -522,6 +522,10 @@ add_executable(db_test1 "${PROJECT_SOURCE_DIR}/test/test.cpp" ) target_link_libraries(db_test1 PRIVATE leveldb gtest) +add_executable(db_test2 +"${PROJECT_SOURCE_DIR}/test/test2.cpp" + ) +target_link_libraries(db_test2 PRIVATE leveldb gtest) add_executable(db_test_bench "${PROJECT_SOURCE_DIR}/test/benchmark_4leveldb.cpp" ) diff --git a/db/db_impl.cc b/db/db_impl.cc index 655c597..ba5ef9e 100644 --- a/db/db_impl.cc +++ b/db/db_impl.cc @@ -1646,12 +1646,6 @@ std::vector> DBImpl::WriteValueLog( return res; } -void DBImpl::writeValueLogForCompaction(WritableFile* target_file, - std::vector values) { - for (int i = 0; i < values.size(); i++) { - target_file->Append(values[i]); - } -} void DBImpl::addNewValueLog() { valuelogfile_number_ = versions_->NewFileNumber(); @@ -1837,24 +1831,23 @@ void DBImpl::GarbageCollect() { // 更新当前偏移 current_offset += sizeof(uint64_t); - // Now seek to the actual data position and read the value - cur_valuelog.seekg(current_offset); - char* value_buf = new char[val_len]; - cur_valuelog.read(value_buf, val_len); - if (!cur_valuelog.good()) { - delete[] key_buf; - delete[] key_buf_len; - delete[] value_buf_len; - delete[] value_buf; - cur_valuelog.close(); - std::cerr << "Failed to read file: " << valuelog_name << std::endl; - break; - } + // // Now seek to the actual data position and read the value + // cur_valuelog.seekg(current_offset); + // char* value_buf = new char[val_len]; + // cur_valuelog.read(value_buf, val_len); + // if (!cur_valuelog.good()) { + // delete[] key_buf; + // delete[] key_buf_len; + // delete[] value_buf_len; + // delete[] value_buf; + // cur_valuelog.close(); + // std::cerr << "Failed to read file: " << valuelog_name << std::endl; + // break; + // } current_offset += val_len; - // Assign the read value data to the Slice - value = Slice(value_buf, val_len); - // std::cout< value)override; std::vector> WriteValueLog( std::vector> value) override; - void writeValueLogForCompaction(WritableFile* target_file, - std::vector value); void addNewValueLog() override EXCLUSIVE_LOCKS_REQUIRED(mutex_); ; std::pair getNewValuelog(); // use for compaction diff --git a/db/dbformat.h b/db/dbformat.h index a89056f..97cd1bb 100644 --- a/db/dbformat.h +++ b/db/dbformat.h @@ -45,7 +45,7 @@ static const int kMaxMemCompactLevel = 2; static const int kReadBytesPeriod = 1048576; // maximum size of value_log file -static const int value_log_size=4<<14; +static const int value_log_size=4<<24; } // namespace config diff --git a/db/write_batch.cc b/db/write_batch.cc index 6030f47..6275892 100644 --- a/db/write_batch.cc +++ b/db/write_batch.cc @@ -140,7 +140,7 @@ class ValueLogInserter : public WriteBatch::Handler { Slice new_value; std::string buf; if(value.size()<100){ - buf+=(char)(0x00); + buf+=(char)(0x00);// should set in key buf.append(value.data(),value.size()); } else{ @@ -149,13 +149,12 @@ class ValueLogInserter : public WriteBatch::Handler { kv.push_back({key,value}); auto res=db_->WriteValueLog(kv); PutVarint64(&buf,res[0].first); - // PutVarint64(&buf,res[0].second.first); - // PutVarint64(&buf,res[0].second.second); PutVarint64(&buf,res[0].second); } new_value=Slice(buf); writeBatch_.Put(key,new_value); } + void Delete(const Slice& key) override { writeBatch_.Delete(key); } diff --git a/test/test.cpp b/test/test.cpp index a75512c..7b1e506 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -16,179 +16,110 @@ Status OpenDB(std::string dbName, DB **db) { return DB::Open(options, dbName, db); } -bool CompareFieldArray(const FieldArray &a, const FieldArray &b) { - if (a.size() != b.size()) return false; - for (size_t i = 0; i < a.size(); ++i) { - if (a[i].first != b[i].first || a[i].second != b[i].second) return false; - } - return true; -} -bool CompareKey(const std::vector a, std::vector b) { - if (a.size() != b.size()){ - return false; - } - for (size_t i = 0; i < a.size(); ++i) { - if (a[i] != b[i]){ - return false; +TEST(Test, CheckGetFields) { + DB *db; + WriteOptions writeOptions; + ReadOptions readOptions; + if(OpenDB("testdb_for_XOY", &db).ok() == false) { + std::cerr << "open db failed" << std::endl; + abort(); } - } - return true; -} + std::string key1 = "k_1"; + + FieldArray fields1 = { + {"name", "Customer#000000001"}, + {"address", "IVhzIApeRb"}, + {"phone", "25-989-741-2988"} + }; -std::string SerializeValue(const FieldArray& fields){ - std::string res_=""; - PutVarint64(&res_,(uint64_t)fields.size()); - for(auto pr:fields){ - PutLengthPrefixedSlice(&res_, pr.first); - PutLengthPrefixedSlice(&res_, pr.second); - } - return res_; -} + auto value1=SerializeValue(fields1); - // 鍙嶅簭鍒楀寲涓哄瓧娈垫暟缁? -void DeserializeValue(const std::string& value_str,FieldArray* res){ - Slice slice=Slice(value_str.c_str()); - uint64_t siz; - bool tmpres=GetVarint64(&slice,&siz); - assert(tmpres); - res->clear(); - for(int i=0;iemplace_back(value_name,value); - } -} + db->Put(WriteOptions(), key1, value1); -Status Get_keys_by_field(DB *db,const ReadOptions& options, const Field field,std::vector *keys){ - auto it=db->NewIterator(options); - it->SeekToFirst(); - keys->clear(); - while(it->Valid()){ - auto val=it->value(); - FieldArray arr; - auto str_val=std::string(val.data(),val.size()); - DeserializeValue(str_val,&arr); - for(auto pr:arr){ - if(pr.first==field.first&&pr.second==field.second){ - Slice key=it->key(); - keys->push_back(std::string(key.data(),key.size())); - break; - } + // 璇诲彇骞跺弽搴忓垪鍖? + std::string value_ret; + FieldArray res1; + + db->Get(ReadOptions(), key1, &value_ret); + DeserializeValue(value_ret, &res1); + for(auto pr:res1){ + std::cout<Next(); - } - delete it; - return Status::OK(); -} + ASSERT_TRUE(CompareFieldArray(fields1, res1)); -// TEST(Test, CheckGetFields) { -// DB *db; -// WriteOptions writeOptions; -// ReadOptions readOptions; -// if(OpenDB("testdb_for_XOY", &db).ok() == false) { -// std::cerr << "open db failed" << std::endl; -// abort(); -// } -// std::string key1 = "k_1"; + db->Delete(WriteOptions(),key1); -// FieldArray fields1 = { -// {"name", "Customer#000000001"}, -// {"address", "IVhzIApeRb"}, -// {"phone", "25-989-741-2988"} -// }; - -// auto value1=SerializeValue(fields1); - -// db->Put(WriteOptions(), key1, value1); + std::cout<<"get serialized value done"<Get(ReadOptions(), key1, &value_ret); -// DeserializeValue(value_ret, &res1); -// for(auto pr:res1){ -// std::cout< keys; + std::vector target_keys; + for(int i=0;i<10000;i++){ + std::string key=std::to_string(rand()%10000)+"_"+std::to_string(i);//random for generate nonincreasing keys + FieldArray fields={ + {"name", key}, + {"address", std::to_string(rand()%7)}, + {"phone", std::to_string(rand()%114514)} + }; + if(rand()%5==0){ + fields[0].second="special_key"; + target_keys.push_back(key); + } + keys.push_back(key); + db->Put(WriteOptions(),key,SerializeValue(fields)); + } + std::sort(target_keys.begin(),target_keys.end()); + std::vector key_res; + Get_keys_by_field(db,ReadOptions(),{"name", "special_key"},&key_res); + ASSERT_TRUE(CompareKey(key_res, target_keys)); + std::cout<<"get key by field done"<Delete(WriteOptions(),s); + } + delete db; +} -// db->Delete(WriteOptions(),key1); - -// std::cout<<"get serialized value done"< keys; -// std::vector target_keys; -// for(int i=0;i<10000;i++){ -// std::string key=std::to_string(rand()%10000)+"_"+std::to_string(i);//random for generate nonincreasing keys -// FieldArray fields={ -// {"name", key}, -// {"address", std::to_string(rand()%7)}, -// {"phone", std::to_string(rand()%114514)} -// }; -// if(rand()%5==0){ -// fields[0].second="special_key"; -// target_keys.push_back(key); -// } -// keys.push_back(key); -// db->Put(WriteOptions(),key,SerializeValue(fields)); -// } -// std::sort(target_keys.begin(),target_keys.end()); -// std::vector key_res; -// Get_keys_by_field(db,ReadOptions(),{"name", "special_key"},&key_res); -// ASSERT_TRUE(CompareKey(key_res, target_keys)); -// std::cout<<"get key by field done"<Delete(WriteOptions(),s); -// } -// delete db; -// } - -// TEST(Test, LARGE_DATA_COMPACT_TEST) { -// DB *db; -// WriteOptions writeOptions; -// ReadOptions readOptions; -// if(OpenDB("testdb_for_XOY_large", &db).ok() == false) { -// std::cerr << "open db failed" << std::endl; -// abort(); -// } -// std::vector values; -// for(int i=0;i<500000;i++){ -// std::string key=std::to_string(i); -// std::string value; -// for(int j=0;j<1000;j++){ -// value+=std::to_string(i); -// } -// values.push_back(value); -// db->Put(writeOptions,key,value); -// } -// for(int i=0;i<500000;i++){ -// std::string key=std::to_string(i); -// std::string value; -// Status s=db->Get(readOptions,key,&value); -// assert(s.ok()); -// if(values[i]!=value){ -// std::cout< values; + for(int i=0;i<500000;i++){ + std::string key=std::to_string(i); + std::string value; + for(int j=0;j<1000;j++){ + value+=std::to_string(i); + } + values.push_back(value); + db->Put(writeOptions,key,value); + } + // for(int i=0;i<500000;i++){ + // std::string key=std::to_string(i); + // std::string value; + // Status s=db->Get(readOptions,key,&value); + // assert(s.ok()); + // if(values[i]!=value){ + // std::cout< +#include + +using namespace std::chrono; +using namespace leveldb; + +using Field=std::pair; +using FieldArray=std::vector>; + +int data_number=100000; + +Status OpenDB(std::string dbName, DB **db) { + Options options; + options.max_file_size=16*1024; + options.write_buffer_size=32*1024; + options.create_if_missing = true; + return DB::Open(options, dbName, db); +} + + +TEST(Test, Garbage_Collect_TEST) { + DB *db; + WriteOptions writeOptions; + ReadOptions readOptions; + if(OpenDB("testdb_for_XOY_large", &db).ok() == false) { + std::cerr << "open db failed" << std::endl; + abort(); + } + std::vector values; + for(int i=0;iPut(writeOptions,key,value); + } + // for(int i=0;iPut(writeOptions,key,value); + // } + // Measure GC time + auto start_time = high_resolution_clock::now(); + db->TEST_GarbageCollect(); + auto end_time = high_resolution_clock::now(); + + auto duration = duration_cast(end_time - start_time); + std::cout << "GC finished. Time taken: " << duration.count() << " ms" << std::endl; + + + for(int i=0;iGet(readOptions,key,&value); + assert(s.ok()); + if(values[i]!=value){ + std::cout< +using namespace leveldb; + +using Field=std::pair; +using FieldArray=std::vector>; namespace leveldb { @@ -155,7 +159,13 @@ bool GetLengthPrefixedSlice(Slice* input, Slice* result) { } } -// 判断文件是否为 valuelog 文件 +/** + * @brief 判断文件是否是 valuelog 文件 + * + * @param filename 文件名(包含路径或纯文件名) + * @return true 如果是 valuelog 文件 + * @return false 如果不是 valuelog 文件 + */ bool IsValueLogFile(const std::string& filename) { // 检查文件是否以 ".valuelog" 结尾 const std::string suffix = ".valuelog"; @@ -163,7 +173,13 @@ bool IsValueLogFile(const std::string& filename) { filename.substr(filename.size() - suffix.size()) == suffix; } -// 示例:解析 sstable 中的元信息 +/** + * @brief 解析存储值,提取 valuelog_id 和 offset 信息 + * + * @param stored_value 存储值的字符串形式(格式为 "valuelog_id|offset") + * @param valuelog_id 输出的 ValueLog 文件 ID + * @param offset 输出的记录偏移量 + */ void ParseStoredValue(const std::string& stored_value, uint64_t& valuelog_id, uint64_t& offset) { // 假设 stored_value 格式为:valuelog_id|offset @@ -172,8 +188,12 @@ void ParseStoredValue(const std::string& stored_value, uint64_t& valuelog_id, GetVarint64(&tmp, &offset); } -// 示例:获取 ValueLog 文件 ID -// 示例:获取 ValueLog 文件 ID +/** + * @brief 根据文件名提取 ValueLog 文件的 ID + * + * @param valuelog_name 文件名(例如 "123.valuelog") + * @return uint64_t 提取的 ValueLog 文件 ID + */ uint64_t GetValueLogID(const std::string& valuelog_name) { // 获取文件名部分(假设文件名格式为 "number.extension") @@ -218,4 +238,75 @@ void SplitIntoChunks(const std::set& files, int num_workers, } } + +bool CompareFieldArray(const FieldArray &a, const FieldArray &b) { + if (a.size() != b.size()) return false; + for (size_t i = 0; i < a.size(); ++i) { + if (a[i].first != b[i].first || a[i].second != b[i].second) return false; + } + return true; +} + +bool CompareKey(const std::vector a, std::vector b) { + if (a.size() != b.size()){ + return false; + } + for (size_t i = 0; i < a.size(); ++i) { + if (a[i] != b[i]){ + return false; + } + } + return true; +} + +std::string SerializeValue(const FieldArray& fields){ + std::string res_=""; + PutVarint64(&res_,(uint64_t)fields.size()); + for(auto pr:fields){ + PutLengthPrefixedSlice(&res_, pr.first); + PutLengthPrefixedSlice(&res_, pr.second); + } + return res_; +} + +void DeserializeValue(const std::string& value_str,FieldArray* res){ + Slice slice=Slice(value_str.c_str()); + uint64_t siz; + bool tmpres=GetVarint64(&slice,&siz); + assert(tmpres); + res->clear(); + for(int i=0;iemplace_back(value_name,value); + } +} + +Status Get_keys_by_field(DB *db,const ReadOptions& options, const Field field,std::vector *keys){ + auto it=db->NewIterator(options); + it->SeekToFirst(); + keys->clear(); + while(it->Valid()){ + auto val=it->value(); + FieldArray arr; + auto str_val=std::string(val.data(),val.size()); + DeserializeValue(str_val,&arr); + for(auto pr:arr){ + if(pr.first==field.first&&pr.second==field.second){ + Slice key=it->key(); + keys->push_back(std::string(key.data(),key.size())); + break; + } + } + it->Next(); + } + delete it; + return Status::OK(); +} + + } // namespace leveldb diff --git a/util/coding.h b/util/coding.h index 5a5c503..4c7cfa2 100644 --- a/util/coding.h +++ b/util/coding.h @@ -18,6 +18,8 @@ #include "leveldb/slice.h" #include "port/port.h" +#include "leveldb/db.h" + namespace leveldb { @@ -126,6 +128,13 @@ uint64_t GetValueLogID(const std::string& valuelog_name); void SplitIntoChunks(const std::set& files, int num_workers, std::vector>* chunks); +bool CompareFieldArray(const FieldArray &a, const FieldArray &b); +bool CompareKey(const std::vector a, std::vector b); +std::string SerializeValue(const FieldArray& fields); +void DeserializeValue(const std::string& value_str,FieldArray* res); +Status Get_keys_by_field(DB *db,const ReadOptions& options, const Field field,std::vector *keys); + + } // namespace leveldb #endif // STORAGE_LEVELDB_UTIL_CODING_H_ diff --git a/设计文档.assets/ValueLog-17325302026895.png b/设计文档.assets/ValueLog-17325302026895.png new file mode 100644 index 0000000..a24e45d Binary files /dev/null and b/设计文档.assets/ValueLog-17325302026895.png differ diff --git a/设计文档.assets/ValueLog-17325304026617.png b/设计文档.assets/ValueLog-17325304026617.png new file mode 100644 index 0000000..fd23573 Binary files /dev/null and b/设计文档.assets/ValueLog-17325304026617.png differ diff --git a/设计文档.assets/ValueLog-17325310409799.png b/设计文档.assets/ValueLog-17325310409799.png new file mode 100644 index 0000000..1a3d356 Binary files /dev/null and b/设计文档.assets/ValueLog-17325310409799.png differ diff --git a/设计文档.assets/ValueLog.png b/设计文档.assets/ValueLog.png new file mode 100644 index 0000000..48a7570 Binary files /dev/null and b/设计文档.assets/ValueLog.png differ diff --git a/设计文档.assets/d85456f5f58abb55d9e83f3c020f0b7.png b/设计文档.assets/d85456f5f58abb55d9e83f3c020f0b7.png new file mode 100644 index 0000000..0a18951 Binary files /dev/null and b/设计文档.assets/d85456f5f58abb55d9e83f3c020f0b7.png differ diff --git a/设计文档.assets/image-20241125122706607.png b/设计文档.assets/image-20241125122706607.png new file mode 100644 index 0000000..fceb136 Binary files /dev/null and b/设计文档.assets/image-20241125122706607.png differ diff --git a/设计文档.assets/image-20241208162802629.png b/设计文档.assets/image-20241208162802629.png new file mode 100644 index 0000000..a21c175 Binary files /dev/null and b/设计文档.assets/image-20241208162802629.png differ diff --git a/设计文档.assets/image-20241208165823491.png b/设计文档.assets/image-20241208165823491.png new file mode 100644 index 0000000..26e3a74 Binary files /dev/null and b/设计文档.assets/image-20241208165823491.png differ diff --git a/设计文档.assets/image-20241208233143593.png b/设计文档.assets/image-20241208233143593.png new file mode 100644 index 0000000..fa2e12b Binary files /dev/null and b/设计文档.assets/image-20241208233143593.png differ diff --git a/设计文档.assets/image-20241209012054587.png b/设计文档.assets/image-20241209012054587.png new file mode 100644 index 0000000..89f81e3 Binary files /dev/null and b/设计文档.assets/image-20241209012054587.png differ diff --git a/设计文档.assets/image-20241209012231561.png b/设计文档.assets/image-20241209012231561.png new file mode 100644 index 0000000..88d5dfd Binary files /dev/null and b/设计文档.assets/image-20241209012231561.png differ diff --git a/设计文档.assets/image-20241209014113365.png b/设计文档.assets/image-20241209014113365.png new file mode 100644 index 0000000..83ec120 Binary files /dev/null and b/设计文档.assets/image-20241209014113365.png differ diff --git a/设计文档.assets/image-20241209020150442.png b/设计文档.assets/image-20241209020150442.png new file mode 100644 index 0000000..0f90fdf Binary files /dev/null and b/设计文档.assets/image-20241209020150442.png differ diff --git a/设计文档.assets/leveldb_values.png b/设计文档.assets/leveldb_values.png new file mode 100644 index 0000000..af8160e Binary files /dev/null and b/设计文档.assets/leveldb_values.png differ diff --git a/设计文档.md b/设计文档.md new file mode 100644 index 0000000..e555f65 --- /dev/null +++ b/设计文档.md @@ -0,0 +1,599 @@ +# 代码设计 + +## 1.项目概述 + +### 1.1 实现字段查询功能 + +LevelDB 的基本数据结构是由一个 key 和对应的 value 组成,其中 value 是一个简单的字节序列(可以是字符串或二进制数据)。默认情况下,LevelDB 不支持像关系型数据库那样的字段查询功能。然而,在实际应用中,用户可能需要对存储的数据进行更加精细的操作,特别是当值包含多个逻辑字段时,直接使用现有的 LevelDB 接口难以满足需求。 +在本实验中,我们的目标是扩展 LevelDB 的功能,使其 value 支持多字段结构,并实现通过字段值查询对应的 key 的功能。 + +### 1.2 KV 分离 + +在 LevelDB 及其采用的 LSM 树结构中,性能挑战之一在于 Compaction 操作的效率。Compaction 是指将内存中的数据合并到磁盘上的过程,此过程中涉及大量的读写操作,对于系统的整体性能有着重要影响。在 Compaction 时,所有涉及到的旧 sstable 中的键值对都将被写入到新 sstable 中,而 Value 通常比 Key 大得多。如果将 Key 和 Value 分离存储,合并时只涉及 key 写入 sstable 的过程,可以显著减少 Compaction 的开销,从而提升性能。 +基于此我们计划实施键值分离策略。具体而言,键将保持原有的排序方式,而值将被独立存储。这样做可以在不影响查询性能的前提下,大幅降低 Compaction 过程中的数据迁移量,进而减少不必要的磁盘 I/O ,提升系统的合并效率。 + + + + + +## 2.功能设计 + +Andy Pavlo在15445课程中说,完成一个项目,应先写出能够完成正确性要求的代码,再在此基础上提升性能,避免不成熟的优化方式。 + +因此,我们的项目流程将保持每周推进代码进度,在完成目标要求的代码的基础上,不断迭代优化性能。 + +![image-20241125122706607](设计文档.assets/image-20241125122706607.png) + +### 2.1.字段设计 + +- **设计目标**: + - 将 LevelDB 中的 `value` 组织成字段数组,每个数组元素对应一个字段(字段名:字段值)。 + - 字段会被序列化为字符串,然后插入LevelDB。 + - 这些字段可以通过解析字符串得到,字段名与字段值都是字符串类型。 + - 允许任意调整字段。 + - 实现通过字段值查询对应的 `key`。 +- **实现思路**: + +函数 `Put_with_fields` 负责插入含字段的数据。原字段数据经过序列化函数 `SerializeValue` 处理后,函数 `Put_with_fields` 调用 `Put` 将序列化后的字段插入 leveldb。 + +函数 `Get_with_fields` 负责获得含字段的数据。使用 `Get` 从 leveldb 中获取 `key` 和序列化后的 `value`,调用 `ParseValue` 可以将字段反序列化。 + +函数 `Get_keys_by_field` 遍历数据库中的所有键值对,解析每个 `Value`,提取字段数组 `FieldArray`。检查字段数组中是否存在目标字段,如果匹配,则记录其对应的 `Key`。将所有匹配 `key` 汇总到 `keys` 中返回。 + +**初步实现**(第一周 已完成):在 leveldb 内部实现以上功能。内部实现会导致读取时无法区分多字段类型和原生 kv 对,扩展性不足。 + +**后续改进**(第二周):为了解决无法区分多字段类型和原生 kv 对的问题,将以上函数功能实现在用户层级,使 leveldb 内部对多字段类型无感知。 + + + +### 2.2.KV分离 + +#### 设计思路 + +- **KV 分离设计** + + a. 将LevelDB的key-value存储结构进行扩展,分离存储key和value + + b. Key存储在一个LevelDB实例中,LSM-tree中的value为一个指向Value log文件和偏移地址的指针,用户Value存储在Value log中。 + +- **读取操作。** + + KV分离后依然支持点查询与范围查询操作。 + +- **Value log的管理。** + + a.当Value log超过一定大小后通过后台GC操作释放Value log中的无效数据。 + + b.GC能把旧Value log中没有失效的数据写入新的Value log,并更新LSM-tree里的键值对。 + + c.新旧Value log的管理功能。 + +- **确保操作的原子性** + + + +#### 实现思路 + +#### 初步实现(第一周 已完成) + +使用单一Value Log简单的实现KV分离,该实现较为简单,仅需在Put/Get函数内部进行简单修改,但在大数据量场景下性能极差。 + +##### 优点:实现简单,合并时开销小 + +##### 缺点:大数据量下性能极差,不能作为最终方案。 + + + +#### 第二种实现(第二周 已完成) + +对每个SSTable和MemTable建立一个Value Log。该实现相比于初步实现更加复杂,需要在合并时查询所有相关Value Log,并建立新Value Log。此外还要考虑在合并结束后将废弃的Value Log异步删除。 + +**Trick 1.为什么要在Put到MemTable时就放入Value Log而非dump至SS Table时才放入Value Log?** + +原因:将写ValueLog推迟至SSTable并没有减少Put时写入磁盘的总数据量(写ValueLog:ValueLog中写Value,WAl中写Key和Value元数据;不写ValueLog:WAL中写Key和Value),优点是将两次无法并行的写文件操作变为一次写文件操作。但该方法有一个缺陷,即leveldb原生的管理数据的方式是MemTable和SSTable大小相等。而经过这样改变后,MemTable在dump成SSTable后其大小会突然减少(Value全部转移至ValueLog),导致一个SSTable中存储的数据量过少。而原本valuelog的优势(一个SSTable可以放更多键值对使得table cache命中率变高)也将不存在了。我们将两个做法都进行了实现,通过对比性能发现后者不如前者,因此选择保留前者设计。 + +##### 优点:随合并自动GC,无需考虑GC。 + +##### 缺点:合并时开销未能减小。 + +**第三种实现**(第三周):使用相对固定大小的Value Log,例如每个Value Log大小约为2KB。新添加的键值对依次将值计入最新Value Log,当Value Log大小满了之后就创建新Value Log。需要设计一种不改变SSTable内记录Value元数据的GC方法。 + +##### 优点:合并时开销小。 + +##### 缺点:需要设计一种GC方式,能够在异步GC的同时不改变SSTable。 + +## 3. 数据结构设计 + +### KV分离后 Value 结构设计 + +一个Value,开头是使用Varint64存储的FieldNum,表示有FieldNum个Field组成。然后是使用Varint64存储的Field X name size,表示该field的字段名长度,然后是字段名,然后是使用Varint64存储的Field X Value size,表示该field的值长度,然后是值。 + +![leveldb_values](设计文档.assets/leveldb_values.png) + +### ValueLog结构设计 + +**第一版设计** + +使用一个Value Log文件的设计中,我们只需记录Value在Value Log中对应的偏移量和Value长度即可。 + +Value Log中只记录Value值,无需记录元信息。 + +![ValueLog](设计文档.assets/ValueLog-17325302026895.png) + +**第二版设计** + +Value设计为:1字节标志位+Varint64文件ID+Varint64偏移量+Varint64长度。 + +在存储时根据Value大小是否较大选择进行KV分离。若分离则标志位为true,否则标志位为false。 + +日志文件中仍然只需记录Value值即可,无需记录元信息。 + +![ValueLog](设计文档.assets/ValueLog-17325304026617.png) + +**第三版设计** + + + +![ValueLog](设计文档.assets/ValueLog-17325310409799.png) + +这一版设计有些复杂。 + +和第二版设计一样,在Value开头使用一字节标志位表示是否KV分离。 + +如果KV分离,则接下来是Varint64的文件ID和Varint64的文件内offset。 + +在ValueLog中,在开头记录当前会索引到该Value Log的键值对数量Using count。如果Using count==0,则表示该ValueLog不被任何键值对使用,可以删除。 + +**Using count在Value Log添加键值对时进行+1**。 + +**Using count**在**其中任意键值对被合并**,**并且 该键值对由于合并时被更加新的键值对覆盖 或者 该键值对的True using Sign=False**时,进行**-1**。 + +在一个Value通过SSTable索引到Value Log后,其索引到的开头是一个**Value True Using Sign**。该标志位同样是一字节,标志了当前该Value是否是真正的Value。 + +**若标志位为True表示是真正的Value,那么标志位后是Varint64的Value长度+Value本身。** + +**若标志位为False表示不是真正的Value,那么标志位后是Varint64的下一个可能存在有真实对应Value的Value Log文件ID和Varint64的在下一个Value Log文件中的offset。** + +**Value True Using Sign发生变化有两种情况:** + +**1** + +一个键值对由于合并时被更加新的键值对覆盖时,不仅将Using count进行-1,同时也将其Value True Using Sign设置为False。 + +**2** + +在键值对加入到Value Log时,其Value True Using Sign设置为True。 + +当后台异步GC过程检测到一个Value Log的Using count较小时,将对其中Value True Using Sign仍为True的Value做以下处理: + + + +1.将Value对应的数据(设为True的标志位,Value len和Value本身)像新数据一样写入最新的Value Log中。 + +2.将原Value True Using Sign置为False + +3.将原Value Log中标志位后的数据修改为新写入的Value Log的ID和数据所处的Offset。 + +(保证原Value大小大于16,以防Varint64(Value len)+Value len [!CAUTION] +> +> **!注意!在实现过程中发现性能缺陷** + +当合并时由于需要扫描合并的SSTable,要对其中每个Value做读ValueLog操作(因为可能可以更新Value指向的ValueLog),导致一次合并会涉及很多次的ValueLog文件读写,性能过于低效,因此想到了新的操作方法。 + + + +### 3.1 fixsize_valuelog实际设计 + +![d85456f5f58abb55d9e83f3c020f0b7](设计文档.assets/d85456f5f58abb55d9e83f3c020f0b7.png) + +#### 新的`valuelog` 文件的组织方式: + +`valuelog` 文件存储了 **键值对(KV)** 数据,每条记录按照以下格式组织: + +1. **键的长度**(`key_len`):`uint64_t`,标识键的字节长度。 +2. **键**(`key`):实际的键数据,长度为 `key_len`。 +3. **值的长度**(`value_len`):`uint64_t`,标识值的字节长度。 +4. **值**(`value`):实际的值数据,长度为 `value_len`。 + +在sstable中key对应的value位置存储了对应valuelog文件的id和在文件中的offset。 + + + +#### gc过程: + +垃圾回收的核心思想是扫描所有的 `valuelog` 文件,检查文件中的记录是否有效。如果记录的键已失效(比如键在 `sstable` 中不存在或元数据不匹配),则该记录会被忽略,最终删除整个无效的 `valuelog` 文件。 + +#### **详细过程:** + +1. **扫描数据库目录**: + + - 遍历 `valuelog` 文件。 + +2. **处理每个 `valuelog` 文件**: + + - 打开文件,逐条读取记录。 + +3. **读取每条记录**: + + - 按文件结构读取 `key_len`、`key`、`value_len`、`value`。 + - 检查 sstable是否包含该键: + - 如果键不存在(或无效),忽略此条记录。 + - 如果键存在,验证元数据(包括 `valuelog_id` 和 `offset`)。 + - 有效的键值对会被重新 put 进入数据库,sstable中重复的key会在compaction过程中被回收。 + +4. **清理无效文件**: + + - 如果整个 `valuelog` 文件的记录均无效或已被迁移,删除该文件。 + + + +## 4. 接口/函数设计 + +#### 4. 1Value多字段设计 + +##### **4.1.1 数据序列化与反序列化** + +**序列化字段数组为字符串值** + +```cpp +std::string SerializeValue(const FieldArray& fields); +``` + +- **输入**:字段数组 `fields`。 + +- **输出**:序列化后的字符串。 + + + +**反序列化字符串值为字段数组** + +```c++ +void DeserializeValue(const std::string& value_str, FieldArray* res); +``` + +- **输入**:序列化字符串 `value_str`。 +- **输出**:字段数组 `res`。 + + + +##### **4.1.2 数据查询接口** + +**按字段查找键** + +```c++ +Status DB::Get_keys_by_field(const ReadOptions& options, const Field field, std::vector* keys); +``` + +- **输入**: + - 读取选项 `options`。 + - 字段值 `field`。 +- **输出**: + - 操作状态 `Status`。 + - 符合条件的键列表 `keys`。 + + + +##### **4.1.3 判断文件是否为 `valuelog` 文件** + +**判断给定文件是否为 `.valuelog` 格式的文件。** + +```c++ +bool IsValueLogFile(const std::string& filename) { +``` + +**输入**: + +- 文件名 `filename`,可以是完整路径或纯文件名。 + +**输出**: + +- 布尔值 `true`:文件是 `valuelog` 文件。 +- 布尔值 `false`:文件不是 `valuelog` 文件。 + + + +##### **4.1.4 解析 `sstable` 中的元信息** + +**解析 `sstable` 中存储的值,提取 `valuelog_id` 和 `offset` 信息。** + +```c++ +void ParseStoredValue(const std::string& stored_value, uint64_t& valuelog_id, uint64_t& offset); +``` + +- **输入**: + - 存储值 `stored_value`,格式为 `"valuelog_id|offset"`。 +- **输出**: + - `valuelog_id`:解析出的 ValueLog 文件 ID。 + - `offset`:解析出的记录偏移量。 + + + +##### **4.1.5 获取 `ValueLog` 文件 ID** + +**从文件名中提取 ValueLog 文件的 ID(假设文件名格式为 `number.valuelog`)。** + +``` +uint64_t GetValueLogID(const std::string& valuelog_name); +``` + +- **输入**: + - 文件名 `valuelog_name`,可以是完整路径或仅文件名,格式需符合 `number.valuelog`。 +- **输出**: + - `uint64_t` 类型,返回提取的文件 ID。 + + + +#### 4.2 Value Log设计 + +##### **4.2.1 WriteValueLog** + +将一堆键值对的值顺序写入Value Log,用于writebatch写入数据库,以及Value Log GC的时候。两者都会对多个键值对同时操作,因此设计为批处理。 + +函数内将使用写锁保证正确性。同一时间最多只有一个WriteValueLog可以进行。 + +```cpp +std::vector> WriteValueLog(std::vector value); +``` + +- **输入**:一个Slice vector,表示要写入Value Log的Value们。 + +- **输出**:一个std::pair vector,每个pair中:第一个uint64_t是Value Log文件ID,第二个uint64_t是处在Value Log中的偏移量。 + +> [!NOTE] +> +> 在第三版设计中,valuelog中会存储key,所以有部分改动。 + + + +##### **4.2.2 ReadValueLog** + +通过Value Log读取目标键值对的值。 + +函数内将使用读锁保证正确性。在一个ValueLog正在被读取时,GC和WriteValueLog(?)无法对该ValueLog操作。 + +```cpp +Status ReadValueLog(uint64_t file_id, uint64_t offset,Slice* value); +``` + +- **输入**:第一个uint64_t是Value Log文件ID,第二个uint64_t是处在Value Log中的偏移量,第三个是指向要传回的value的指针。 +- **输出**:一个Status,表示是否成功传回对应Value。 + +> [!NOTE] +> +> 在第三版设计中,valuelog中会存储key,所以有部分改动。 + + + +##### **4.2.3 测试GC** + +调用`MaybeScheduleGarbageCollect()`来安排一个后台线程执行垃圾回收任务。它会等待所有已安排的垃圾回收任务完成,这通过循环检查`background_garbage_collect_scheduled_`标志,并在该标志为真时等待`background_gc_finished_signal_`信号来实现。 + +```cpp +void DBImpl::TEST_GarbageCollect() +``` + + + +##### **4.2.4 调用线程进行GC** + +启动一个新的后台线程执行`BGWorkGC`方法。这里使用了`gc_mutex_.Lock()`来确保线程安全。 + +```cpp +void DBImpl::MaybeScheduleGarbageCollect() +``` + + + +##### **4.2.5 调用负责GC函数** + +调用`BackgroundGarbageCollect()`进行实际的垃圾回收工作。 + +```cpp +void DBImpl::BGWorkGC(void* db) +``` + + + +##### **4.2.6 后台GC函数** + +负责执行后台垃圾回收任务。它确保在进行垃圾回收时,只有一个线程能够访问共享资源,并且在完成任务后通知等待的线程。 + +```cpp +void DBImpl::BackgroundGarbageCollect() +``` + + + +##### **4.2.7 后台GC函数** + +垃圾回收的核心实现。在目前的设计下,它遍历数据库目录中的所有valuelog文件,并尝试回收不再需要的数据。 + +```cpp +void DBImpl::GarbageCollect() +``` + + + +------ + + + +## 5. 功能测试 + +### 5.1**单元测试(测试用例)**: + +#### 依据我们的设计,每周的工作内容完成后,都将对当前完成的功能进行正确性检验。以下以第一周我们完成的功能为例: + +#### 第一周 + +**字段数组的存储与读取:** + +验证了 `Put_with_fields` 和 `Get_with_fields` 的正确性,确保字段数组可以正确序列化存储并反序列化读取。 + +**基于字段的键查询:** + +验证了 `Get_keys_by_field` 的逻辑,确保能够根据字段值查找所有匹配的键。 + +**Key Value分离:** + +并未额外设计,通过上两个功能的正确运行能够证明Key Value分离的初步实现大体是正确的。 + +```c++ +#include "gtest/gtest.h" +#include "leveldb/env.h" +#include "leveldb/db.h" +using namespace leveldb; + +constexpr int value_size = 2048; +constexpr int data_size = 128 << 20; + +Status OpenDB(std::string dbName, DB **db) { + Options options; + options.create_if_missing = true; + return DB::Open(options, dbName, db); +} + +TEST(TestTTL, OurTTL) { + DB *db; + WriteOptions writeOptions; + ReadOptions readOptions; + if(OpenDB("testdb_for_XOY", &db).ok() == false) { + std::cerr << "open db failed" << std::endl; + abort(); + } + std::string key = "k_1"; + + std::string key1 = "k_2"; + + FieldArray fields = { + {"name", "Customer#000000001"}, + {"address", "IVhzIApeRb"}, + {"phone", "25-989-741-2988"} + }; + + FieldArray fields1 = { + {"name", "Customer#000000001"}, + {"address", "abc"}, + {"phone", "def"} + }; + + db->Put_with_fields(WriteOptions(), key, fields); + + db->Put_with_fields(WriteOptions(), key1, fields1); + + // 读取并反序列化 + FieldArray value_ret; + db->Get_with_fields(ReadOptions(), key, &value_ret);; + for(auto pr:value_ret){ + std::cout< v; + db->Get_keys_by_field(ReadOptions(),fields[0],&v); + for(auto s:v)std::cout<