|
|
@ -1,5 +1,3 @@ |
|
|
|
|
|
|
|
|
|
|
|
# LEVELDB TTL |
|
|
|
|
|
|
|
#### 小组成员 |
|
|
@ -16,11 +14,11 @@ |
|
|
|
|
|
|
|
其中每个分支对应的内容为: |
|
|
|
|
|
|
|
master分支:主分支:第二版TTL设计。 |
|
|
|
master 分支:主分支:第二版TTL设计。 |
|
|
|
|
|
|
|
xry分支:第一版TTL设计,完成了在memtable中的带有TTL数据的存取(即在小数据量情况下可以正确运行测试脚本中的第一个测试)。 |
|
|
|
xry 分支:第一版TTL设计,完成了在 memtable 中的带有 TTL 数据的存取(即在小数据量情况下可以正确运行测试脚本中的第一个测试)。 |
|
|
|
|
|
|
|
xxy分支、new_version分支:第二版TTL设计。其中new_version分支与master分支内容相同。 |
|
|
|
xxy 分支、new_version 分支:第二版 TTL 设计。其中 new_version 分支与 master 分支内容相同。 |
|
|
|
|
|
|
|
### 设计思路 |
|
|
|
|
|
|
@ -76,7 +74,7 @@ Slice input(rep_); |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
之后,使用MemTable::Add将解析出来的数据重新编码,并插入至memtable,这种新的编码形式同样适用于SSTable。 |
|
|
|
之后,使用MemTable::Add将解析出来的数据重新编码,并插入至memtable。对于SSTable来说,InternalKey中的SequenceNumber和Type被隐藏,但存储数据的形式未发生改变。 |
|
|
|
|
|
|
|
```c++ |
|
|
|
void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key, |
|
|
@ -116,7 +114,7 @@ void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key, |
|
|
|
|
|
|
|
我们的第一版TTL存储设计如下: |
|
|
|
|
|
|
|
![old_record](ttl.assets/old_record-17307966031002.png) |
|
|
|
![old_record-17307966031002](ttl.assets/old_record-17307966031002.png) |
|
|
|
|
|
|
|
在我们最初的思考中,TTL与SequenceNumber、Type一样,属于和主要的存储内容(key,value)分割开的存储数据,因此我们选择将TTL加在SequenceNumber和Type后面(不加在中间和前面,以免破坏原先InternalKey的结构)。 |
|
|
|
|
|
|
@ -126,7 +124,7 @@ void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key, |
|
|
|
|
|
|
|
我们的第二版TTL存储设计如下: |
|
|
|
|
|
|
|
![old_record](ttl.assets/old_record-17308259248204.png) |
|
|
|
![old_record-17308259248204](ttl.assets/old_record-17308259248204.png) |
|
|
|
|
|
|
|
由于 leveldb 本身对于 key、value、sequencenumber 的解析已经非常完善并且有些耦合,因此我们想要在不破坏 leveldb 原先的解析方式下完成 TTL 功能,我们想到的最好最轻量级的方式,就是将 TTL 数据作为 Value的一部分(在原 Value 后追加 8byte 作为 TTL )进行存储,仅当要解析 Value 时(即读取数据时或合并时),才会在同时对 TTL 进行解析。由此一来,我们无需改变原本的存储结构。事实证明,我们在很短时间内就完成了这一版 TTL的全部所需功能。 |
|
|
|
|
|
|
@ -134,7 +132,7 @@ void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key, |
|
|
|
|
|
|
|
### 实现流程 |
|
|
|
|
|
|
|
### 第一版设计 |
|
|
|
#### 第一版设计 |
|
|
|
|
|
|
|
首先,我们需要修改在 writebatch 中数据的存取格式(主要是对 Value 的读取需要向后偏移8位)。我们在 WriteBatch 中新增了 Put_for_ttl 函数,将 ttl 放置在 key,value 之前。 |
|
|
|
|
|
|
@ -303,7 +301,11 @@ void TableBuilder::Add(const Slice& key, uint64_t ttl, const Slice& value) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##### 然而,在处理Block中数据的存取时,我们遇到了问题: |
|
|
|
|
|
|
|
![image-20241106203303238](ttl.assets/image-20241106203303238.png) |
|
|
|
|
|
|
|
Leveldb使用Block::Iter对于IndexBlock和dataBlock进行统一的数据读取,在处理dataBlock时,我们期望Block::Iter对数据的处理能够适配ttl,即在key与value中间存有一份TTL。然而在debug过程中我们发现,在改动Block::Iter获取下一个键值对和获取当前键值对的Value的逻辑后(因为要多向后移8字节),使得IndexBlock获取数据出错。在经过一段时间的debug后,我们认为要完成解决这一bug,需要连带改动IndexBlock存取数据的逻辑,而这已经偏离了TTL本身的范围,使得项目的紧耦合度增强,既不轻量也不优雅,因此我们放弃解决这一bug,转而构思出了第二版设计。 |
|
|
|
|
|
|
|
#### 第二版设计 |
|
|
|
|
|
|
@ -359,7 +361,7 @@ Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value, ui |
|
|
|
int len = value.size() + sizeof(uint64_t); |
|
|
|
char* new_data = new char[len]; |
|
|
|
time_t now = time(nullptr); // 获取当前时间,单位为秒 |
|
|
|
ttl += static_cast<uint64_t>(now); // 将当前时间加上 TTL,计算过期时间 |
|
|
|
ttl += static_cast<uint64_t>(now); // 将当前时间加上 TTL,计算过期时间,以过期时间戳作为真正存储的TTL |
|
|
|
|
|
|
|
memcpy(new_data, value.data(), value.size()); // 将实际的值复制到 new_data 中 |
|
|
|
memcpy(new_data + len - sizeof(uint64_t), (char*)(&ttl), sizeof(uint64_t)); // 将 TTL 复制到 new_data 的末尾 |
|
|
@ -491,6 +493,10 @@ bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) { |
|
|
|
|
|
|
|
##### 测试 |
|
|
|
|
|
|
|
1.将原有的两个测试的严格sleep ttl秒,改成sleep(ttl+1)秒,因为我们的设计是以秒为存储单位,可能存在一秒以内的读取误差,所以进行修改。(我们认为ttl无需毫秒/微妙级的数据准确性,一秒之内的误差可以接受,因此采取此种设计) |
|
|
|
|
|
|
|
2.在原有设计上增加了第三个测试,其目的在于:①测试新的数据过期后,是否能够重新查找到旧但未过期的数据。②测试无TTL的接口是否可以正确运行,并且数据不过期。 |
|
|
|
|
|
|
|
```c++ |
|
|
|
#include "gtest/gtest.h" |
|
|
|
#include "leveldb/env.h" |
|
|
@ -612,17 +618,17 @@ TEST(TestTTL, OurTTL) { |
|
|
|
for (int i = 0; i < 10000; i++) { |
|
|
|
std::string key = std::to_string(i); |
|
|
|
std::string value = std::to_string(i); |
|
|
|
db->Put(writeOptions, key, value); |
|
|
|
db->Put(writeOptions, key, value);//插入永不过期的key value(value=key) |
|
|
|
} |
|
|
|
for (int i = 0; i < 10000; i++) { |
|
|
|
std::string key = std::to_string(i); |
|
|
|
std::string value = std::to_string(i*2); |
|
|
|
db->Put(writeOptions, key, value, 30); |
|
|
|
db->Put(writeOptions, key, value, 30);//插入30秒后过期的key value(value=key*2) |
|
|
|
} |
|
|
|
for (int i = 0; i < 10000; i++) { |
|
|
|
std::string key = std::to_string(i); |
|
|
|
std::string value = std::to_string(i*3); |
|
|
|
db->Put(writeOptions, key, value, 15); |
|
|
|
db->Put(writeOptions, key, value, 15);//插入15秒后过期的key value(value=key*3) |
|
|
|
} |
|
|
|
|
|
|
|
for (int i = 0; i < 10000; i++) { |
|
|
@ -630,7 +636,7 @@ TEST(TestTTL, OurTTL) { |
|
|
|
std::string value; |
|
|
|
Status status = db->Get(readOptions, key, &value); |
|
|
|
ASSERT_TRUE(status.ok()); |
|
|
|
ASSERT_TRUE(value==std::to_string(i*3)); |
|
|
|
ASSERT_TRUE(value==std::to_string(i*3));//立刻读取,期望得到value=key*3 |
|
|
|
} |
|
|
|
|
|
|
|
Env::Default()->SleepForMicroseconds((15+1) * 1000000); |
|
|
@ -640,7 +646,7 @@ TEST(TestTTL, OurTTL) { |
|
|
|
std::string value; |
|
|
|
Status status = db->Get(readOptions, key, &value); |
|
|
|
ASSERT_TRUE(status.ok()); |
|
|
|
ASSERT_TRUE(value==std::to_string(i*2)); |
|
|
|
ASSERT_TRUE(value==std::to_string(i*2));//16秒后读取,期望得到value=key*2 |
|
|
|
} |
|
|
|
|
|
|
|
Env::Default()->SleepForMicroseconds((15+1) * 1000000); |
|
|
@ -650,7 +656,7 @@ TEST(TestTTL, OurTTL) { |
|
|
|
std::string value; |
|
|
|
Status status = db->Get(readOptions, key, &value); |
|
|
|
ASSERT_TRUE(status.ok()); |
|
|
|
ASSERT_TRUE(value==std::to_string(i)); |
|
|
|
ASSERT_TRUE(value==std::to_string(i));//32秒后读取,期望得到value=key |
|
|
|
} |
|
|
|
|
|
|
|
delete db; |
|
|
@ -664,3 +670,5 @@ int main(int argc, char** argv) { |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
测试结果 |
|
|
|
![image-20241106220858020](ttl.assets/image-20241106220858020.png) |