1. 1. MyLSM实操
    1. 1.1. 一、环境配置
    2. 1.2. 二、跳表
      1. 1.2.1. 2.1 随机数
      2. 1.2.2. 2.2 前缀++和后缀++重载
      3. 1.2.3. 2.3 跳表get操作
      4. 1.2.4. 2.4 谓词查询
    3. 1.3. 三、memtable
      1. 1.3.1. 3.1加锁插入
      2. 1.3.2. 3.2 活跃表刷写为冻结表
      3. 1.3.3. 3.3 迭代器
        1. 1.3.3.1. 3.3.1 标准容器迭代器
        2. 1.3.3.2. 3.3.2 自定义迭代器
        3. 1.3.3.3. 3.3.3 memtable迭代器
        4. 1.3.3.4. 3.3.4 SearchIterator
        5. 1.3.3.5. 3.3.5 HeapIterator
        6. 1.3.3.6. 3.3.6 MemTableIterator
      4. 1.3.4. 3.4 补充知识
        1. 1.3.4.1. 3.4.1 运算符重载的实现方式
        2. 1.3.4.2. 3.4.2 std::pair 简介
    4. 1.4. 四、SST
      1. 1.4.1. 4.1 Block
        1. 1.4.1.1. 4.1.1 编码和解码
        2. 1.4.1.2. 4.1.2 add_entry
        3. 1.4.1.3. 4.1.3 直接赋值和返回this
      2. 1.4.2. 4.2 SST Builder
      3. 1.4.3. 4.3 file
        1. 1.4.3.1. 4.3.1 std_file
        2. 1.4.3.2. 4.3.2 MmapFile
        3. 1.4.3.3. 4.3.3 FileObj
      4. 1.4.4. 4.4 补充知识
        1. 1.4.4.1. 4.4.1 size_t和uint8_t
        2. 1.4.4.2. 4.4.2 vector::data()
        3. 1.4.4.3. 4.4.3 std::enable_shared_from_this
        4. 1.4.4.4. 4.4.4 hash
        5. 1.4.4.5. 4.4.5 std::string_view
        6. 1.4.4.6. 4.4.6 reinterpret_cast
        7. 1.4.4.7. 4.4.7 **vector::reserve **
        8. 1.4.4.8. 4.4.8 vector::assign
        9. 1.4.4.9. 4.4.9 string构造重载
        10. 1.4.4.10. 4.4.10 string::compare
        11. 1.4.4.11. 4.4.11 share_ptr
        12. 1.4.4.12. 4.4.12 mutable
        13. 1.4.4.13. 4.4.13 匿名函数
        14. 1.4.4.14. 4.4.14 lambda 表达式
        15. 1.4.4.15. 4.4.15 string::assign
        16. 1.4.4.16. 4.4.16 FileObj::create_and_write
        17. 1.4.4.17. 4.4.17 FileObj::read_to_slice
        18. 1.4.4.18. 4.4.18 二分查找范围
    5. 1.5. 五、LSMEngine
      1. 1.5.1. 5.2 compact&&Iterator
        1. 1.5.1.1. 5.2.1 Concact Iterator
        2. 1.5.1.2. 5.2.2TwoMergeIterator
        3. 1.5.1.3. 5.2.3 Size-Tiered Compaction
        4. 1.5.1.4. 5.2.4 Leveling Compaction
        5. 1.5.1.5. 5.2.5 缓存池
        6. 1.5.1.6. 5.2.6 布隆过滤器
      2. 1.5.2. 补充知识
        1. 1.5.2.1. 5.1.1 map
        2. 1.5.2.2. 5.1.2 is_regular_file
        3. 1.5.2.3. 5.1.3 std::stoull()
        4. 1.5.2.4. 5.1.4 std::unordered_map
        5. 1.5.2.5. 5.1.5 std::list::splice()
    6. 1.6. 六、事务
      1. 1.6.1. 6.1 事务管理器
        1. 1.6.1.1. 6.1.1 atomic
      2. 1.6.2. 6.2 Wal
        1. 1.6.2.1. 6.2.1 std::thread
    7. 1.7. 七、兼容Redis
      1. 1.7.1. 7.1 Resp 协议简介
        1. 1.7.1.1. 7.1.1Resp 协议的核心设计目标
        2. 1.7.1.2. 7.1.2 Resp 协议的核心数据类型(Resp 2.0)
        3. 1.7.1.3. 7.1.3 Resp 协议的通信流程(以 “客户端发命令 + 服务器回结果” 为例)
        4. 1.7.1.4. 7.1.4 Resp 3.0:Resp 2.0 的扩展(可选了解)
        5. 1.7.1.5. 7.1.5 Resp 协议的优势与局限性
      2. 1.7.2. 7.2 字符串类型
      3. 1.7.3. 7.3 哈希类型
      4. 1.7.4. 7.4 set类型
      5. 1.7.5. 7.5 RedisSever
        1. 1.7.5.1. 7.5.1 bind
    8. 1.8. 五、问题记录
      1. 1.8.1. 5.1 Memtable友元类
      2. 1.8.2. 5.2 Block::add_entry

MyLSM——本地kv存储引擎

MyLSM实操

一、环境配置

本项目基于容器进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
docker pull ubuntu:22.04
docker run --network=host -it --name my_LSM ubuntu:22.04
docker exec -it my_LSM /bin/bash
apt update
apt install -y unzip curl git build-essential cmake python3 gawk vim gcc g++ clangd python3.10-dev libspdlog-dev
#执行下方指令 不要开梯子
curl -fsSL https://xmake.io/shget.text | bash

vim .bashrc
export XMAKE_ROOT=y
source ~/.bashrc

vim /etc/environment
/root/.local/bin
source /etc/environment
echo 'source /etc/environment '>>/etc/profile

git clone https://github.com/Vanilla-Beauty/tiny-lsm.git --depth 1 -b lab
cd tiny-lsm
mkdir build

xmake

apt install libspdlog-dev

无法下载解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

echo 'export HTTP_PROXY="http://127.0.0.1:7890"' >> ~/.bashrc
echo 'export HTTPS_PROXY="http://127.0.0.1:7890"' >> ~/.bashrc
source ~/.bashrc
curl -I https://github.com

# 1. 安装必要工具
apt update && apt install -y unzip curl git cmake build-essential

# 2. 设置系统级别代理
echo 'http_proxy=http://127.0.0.1:7890' >> /etc/environment
echo 'https_proxy=http://127.0.0.1:7890' >> /etc/environment
source /etc/environment

# 3. 配置 xmake 的代理
xmake g --proxy=http://127.0.0.1:7890

# 4. 启动编译
xmake

vscode插件

xmake clangd CodeLLDB

安装clangd插件时候,如果开发环境采用的是容器,那么也需要在容器里安装一个插件(通过vscode扩展)

关闭spglog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. 关闭日志输出(设置 off 级别)
export SPDLOG_LEVEL=off # 设置环境变量
./your_program # 运行程序,所有日志将被关闭
效果:

所有 spdlog::info()、spdlog::warn()、spdlog::error() 等日志均不会输出。
相当于在代码中调用了 spdlog::set_level(spdlog::level::off)。
2. 重新开启日志输出
方法一:恢复默认级别(如 info)
bash
unset SPDLOG_LEVEL # 清除环境变量(恢复默认行为)
./your_program # 程序将使用代码中设置的日志级别
或显式指定级别:

bash
export SPDLOG_LEVEL=info # 允许 INFO 及以上级别日志
./your_program

二、跳表

跳表服务于memtable

SStable不需要跳表,而是block和index

2.1 随机数

头文件

1
2
//头文件
#include <random>

一种方式:定义

1
2
3
4
5
6
7
// 创建随机数引擎并设置种子
std::random_device rd;
std::mt19937 gen(rd());
// 创建一个生成0到1之间整数的均匀分布
std::uniform_int_distribution<> dis(0, 1);
// 生成随机数
int result = dis(gen);

另一种方式:先声明,后赋值

1
2
3
4
5
6
7
std::uniform_int_distribution<> dis_01;  
std::uniform_int_distribution<> dis_level;
std::mt19937 gen;

dis_01 = std::uniform_int_distribution<>(0, 1);
dis_level = std::uniform_int_distribution<>(0, 1<<maxlevel-1);
gen = std::mt19937(std::random_device()());

random_device非确定性随机数生成器类,位于 <random> 头文件中。它的主要作用是生成高质量的随机种子,确保伪随机数序列的不可预测性。

random_device() 是构造函数创建临时对象。

random_device()()是调用临时对象的运算符()重载,生成随机数种子。

std::mt19937(std::random_device()())是调用mt19913类的构造函数,用random_device()()初始化,创建的临时对象。然后临时对象赋值给gen变量。

2.2 前缀++和后缀++重载

在 C++ 中,++i(前缀递增)和i++(后缀递增)是两种不同的操作,它们的重载方式和返回值类型有明确区别。

1. 前缀递增 (++i) 的特点与实现

1
2
3
4
5
6
BaseIterator& SkipListIterator::operator++() {
if (current) {
current = current->forward_[0];
}
return *this;
}
  • 返回类型BaseIterator&(引用)
  • 操作行为:先修改对象状态(将current指向下一个节点),再返回修改后的对象本身
  • 设计意图:支持链式调用(如++i++j

2. 后缀递增 (i++) 的正确实现方式

后缀递增需要不同的语法和返回值类型:

1
2
3
4
5
6
7
BaseIterator SkipListIterator::operator++(int) {
BaseIterator old = *this; // 保存旧状态
if (current) {
current = current->forward_[0];
}
return old; // 返回旧状态
}
  • 参数特点:后缀递增有一个额外的int参数(仅用于区分前缀 / 后缀,实际不使用)
  • 返回类型BaseIterator(值类型)
  • 操作行为:先保存旧状态,再修改对象,最后返回旧状态

3. 前缀与后缀的核心区别

特性 前缀递增 ++i 后缀递增 i++
参数 无参数 隐含int参数(仅作标记)
返回值类型 引用(T&),便于链式调用 值(T),返回旧状态
执行顺序 先修改,再返回 先保存旧值,再修改,最后返回
效率 更高(无需创建临时对象) 可能产生临时对象
  1. 为什么你的代码只能实现++i

返回类型是引用:前缀递增返回引用,支持链式操作(++++i);后缀递增必须返回值(旧状态)。

没有int参数:后缀递增的重载需要一个int参数来与前缀版本区分,你的代码缺少这个参数。

语义不同:前缀递增的语义是 “先递增,再使用”,后缀是 “先使用,再递增”,两者的实现逻辑不同。

  1. 为什么优先使用前缀递增?

效率更高:前缀递增无需创建临时对象,尤其对自定义类型更明显。

语义更清晰++i明确表示 “先递增”,符合多数场景的需求。

兼容性更好:标准库迭代器和 STL 算法都优先使用前缀递增。

6. for`循环中的常见场景:效果相同

  1. 循环条件仅依赖迭代器移动

for循环的结构为:

1
2
3
for (auto it = container.begin(); it != container.end(); ++it) {
// 使用*it
}

1
2
3
for (auto it = container.begin(); it != container.end(); it++) {
// 使用*it
}

两者效果完全相同,因为:

  • ++it:先移动迭代器,再判断条件(符合逻辑)
  • it++:先记录旧迭代器用于判断条件,再移动迭代器
    (本质上也是移动后再判断,因为it++的返回值会被丢弃)
  1. 原理:返回值被忽略

for循环的第三个表达式(迭代器更新)的返回值会被直接丢弃,因此:

  • ++it的返回值(新迭代器引用)被丢弃
  • it++的返回值(旧迭代器副本)也被丢弃
    最终都会让迭代器移动到下一个位置,不影响循环逻辑。

7.特殊场景:后缀递增可能引发问题

  1. 迭代器移动与返回值同时使用

如果在迭代器递增的同时使用其返回值,可能导致逻辑错误:

1
2
3
4
5
// 错误示例(不建议在for循环中这样写)
for (auto it = container.begin(); it != container.end(); someFunc(it++)) {
// someFunc使用的是it++的返回值(旧迭代器)
// 但循环判断条件用的是移动后的it,可能导致逻辑混乱
}

此时it++的返回值(旧迭代器)和it本身(新迭代器)会产生语义差异,可能引发 bug。

  1. 自定义迭代器的异常行为

如果迭代器的operator++(int)实现有误(如未正确保存旧状态),使用it++可能导致:

  • 迭代器跳过元素
  • 循环条件判断错误
  • 甚至程序崩溃
    (但正规容器的迭代器不会有这种问题)

8. 性能差异:前缀递增更优

虽然功能相同,但前缀递增的性能通常更好,原因如下:

  1. 内置类型的性能差异

对于int等基本类型:

  • ++i:直接修改值并返回引用(无额外开销)

  • i++:需要创建临时副本保存旧值,再返回副本(有拷贝开销)

    1
    2
    3
    int i = 0;
    ++i; // 直接修改i,无副本
    i++; // 创建临时变量保存0,再将i改为1,返回临时变量
  1. 自定义迭代器的性能差异

对于自定义迭代器(如std::vector<int>::iterator):

  • ++it:直接移动迭代器并返回自身(无拷贝)
  • it++:需要创建迭代器副本保存旧状态,再返回副本
    (尤其是当迭代器是复杂类时,拷贝开销更明显)
  1. 编译器优化的局限性

虽然现代编译器可能优化i++的拷贝开销,但:

  • 前缀递增的逻辑更直接,优化空间更大
  • 对于不支持拷贝优化的场景(如const迭代器),it++的开销无法避免

9.最佳实践:优先使用前缀递增

理由

  • 符合 “先修改后使用” 的语义,逻辑更清晰
  • 避免潜在的性能开销(尤其是自定义迭代器)
  • 与 STL 标准库的推荐一致(如C++ Core Guidelines建议优先用++i

示例对比

1
2
3
4
5
6
7
8
9
// 推荐写法(前缀递增)
for (auto it = vec.begin(); it != vec.end(); ++it) {
process(*it);
}

// 等效但不推荐写法(后缀递增)
for (auto it = vec.begin(); it != vec.end(); it++) {
process(*it);
}

2.3 跳表get操作

image-20250530102540155

非常经典

1
2
3
4
5
6
7
8
9
10
auto current=head;
int i=max_level-1;
while (i>=0) {
while(current->forward_[i]&&current->forward_[i]->key_<key)
{
current=current->forward_[i];
}
i--;
}
current=current->forward_[0];

以查找key=13为例,current>forward_[2]=1 1<13

1
2
3
4
5
6
7
8
9
xmake clean
xmake
xmake run test_skiplist

调试方法:
gdb 可执行文件
run
bt
看段错误在哪里报错了

2.4 谓词查询

三、memtable

3.1加锁插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MemTable::put(const std::string &key, const std::string &value,
uint64_t tranc_id) {
spdlog::trace("MemTable--put({}, {}, {}) called", key, value, tranc_id);

std::unique_lock<std::shared_mutex> lock1(cur_mtx);
put_(key, value, tranc_id);
if (current_table->get_size() >
TomlConfig::getInstance().getLsmPerMemSizeLimit()) {
// 冻结当前表还需要获取frozen_mtx的写锁
std::unique_lock<std::shared_mutex> lock2(frozen_mtx);
frozen_cur_table_();
spdlog::debug("MemTable--Current table size exceeded limit. Frozen and "
"created new table.");
}
}

std::unique_lock<std::shared_mutex> lock1(cur_mtx);

  1. std::shared_mutex
  • 特性:一种读写锁(Read-Write Lock),允许多个线程同时获取读锁(共享访问),但写锁必须独占(互斥访问)。
  • 适用场景:适合 “多读少写” 的场景,如缓存系统、数据库索引等。
  1. std::unique_lock
  • 特性:一个模板类,提供对互斥锁的高级管理。与std::lock_guard相比,它更灵活,可以手动控制锁的获取和释放。
  • 作用:
    • 构造时:自动调用cur_mtx.lock()获取写锁(独占访问)。
    • 析构时:自动调用cur_mtx.unlock()释放锁,避免死锁。
  1. std::shared_lock
1
2
3
4
// 独占锁(写锁)
std::unique_lock<std::shared_mutex> lock(cur_mtx); // 构造时自动加锁
current_table->insert(key, value); // 修改共享数据
// 离开作用域时,lock自动释放锁
1
2
3
4
// 共享锁(读锁)
std::shared_lock<std::shared_mutex> lock(cur_mtx); // 允许多线程同时读
auto value = current_table->get(key); // 读取共享数据
// 离开作用域时,lock自动释放锁
当前状态 读锁请求(共享锁) 写锁请求(独占锁)
无锁 立即获取 立即获取
已有读锁(N 个) 第 N+1 个读锁立即获取 阻塞,需等待所有读锁释放
已有写锁 阻塞,需等待写锁释放 阻塞,需等待写锁释放

3.2 活跃表刷写为冻结表

frozen_tables.push_front(std::move(current_table));

std::move 将左值转换为右值引用,从而允许资源的所有权转移,避免不必要的深拷贝。

  1. 避免不必要的拷贝

current_table 是一个 std::shared_ptr<SkipList>,它是一个智能指针,管理着堆上的 SkipList 对象。如果直接使用 push_front(current_table),会触发 shared_ptr 的拷贝构造函数:

1
2
// 拷贝构造:引用计数+1,复制指针
shared_ptr(const shared_ptr& r) noexcept;

这会增加引用计数,并创建一个新的 shared_ptr 对象,指向同一个 SkipList。虽然 shared_ptr 的拷贝操作本身很快(只是指针复制和引用计数更新),但对于频繁的内存表切换操作,这种开销仍然是不必要的。

  1. 实现所有权转移

通过使用 std::move,我们将 current_table 转换为右值引用,触发 shared_ptr 的移动构造函数:

1
2
// 移动构造:引用计数不变,转移所有权
shared_ptr(shared_ptr&& r) noexcept;

移动构造后,current_table 会变成一个空指针(nullptr),而 frozen_tables 中的新元素将接管原来 current_table 指向的 SkipList 对象的所有权。这样可以避免引用计数的增加和减少操作,提高效率。

  1. 内存管理的正确性

在这个场景中,current_table 是一个临时对象,即将被替换为新的 SkipList。通过 std::move 转移所有权后,current_table 会被安全地重置为空指针,避免了资源泄漏。

  1. 符合代码逻辑

从业务逻辑上看,当我们将当前内存表(current_table)冻结并加入到 frozen_tables 中时,实际上是在转移所有权。current_table 不应该再被使用,因为它已经被冻结并等待刷新到磁盘。使用 std::move 清晰地表达了这种所有权的转移。

不使用 std::move

1
2
3
// 引用计数从1变为2
frozen_tables.push_front(current_table);
// current_table 仍然指向同一个对象,引用计数为2

使用 std::move

1
2
3
// 所有权转移,引用计数保持为1
frozen_tables.push_front(std::move(current_table));
// current_table 现在是nullptr,引用计数为0

3.3 迭代器

迭代器是一种抽象的访问手段,用来遍历容器中的元素,而不需要关心底层结构(数组?链表?红黑树?)。

3.3.1 标准容器迭代器

std::vector nums = {1, 2, 3, 4, 5};

1
2
3
4
// 使用迭代器遍历
for (std::vector<int>::iterator it = nums.begin(); it != nums.end(); ++it) {
std::cout << *it << " "; // 访问元素
}

3.3.2 自定义迭代器

迭代器定义在类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyContainer {
public:
int data[5] = {10, 20, 30, 40, 50};

class Iterator {
private:
int* ptr;
public:
Iterator(int* p) : ptr(p) {}
int& operator*() const { return *ptr; }
Iterator& operator++() { ++ptr; return *this;}
bool operator!=(const Iterator& other) const {return ptr != other.ptr;}
};

Iterator begin() { return Iterator(data); }
Iterator end() { return Iterator(data + 5); }
};
1
2
3
4
5
6
7
int main() {
MyContainer mc;

for (auto it = mc.begin(); it != mc.end(); ++it) {
std::cout << *it << " ";
}
}

迭代器定义在类外

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MyContainer {
public:
int data[5] = {10, 20, 30, 40, 50};

// begin() / end() 返回外部定义的迭代器
class MyIterator begin();
class MyIterator end();
};
// ===== 自定义迭代器类(定义在类外) =====
class MyIterator {
private:
int* ptr;
public:
MyIterator(int* p) : ptr(p) {}

int& operator*() const { return *ptr; }

MyIterator& operator++() { ++ptr; return *this;}

bool operator!=(const MyIterator& other) const { return ptr != other.ptr; }
};
// ===== begin()/end() 函数实现 =====
MyIterator MyContainer::begin() { return MyIterator(data); }
MyIterator MyContainer::end() {return MyIterator(data + 5); }

// ===== 使用示例 =====
int main() {
MyContainer mc;
for (MyIterator it = mc.begin(); it != mc.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}

迭代器抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 抽象迭代器接口(类似 std::iterator)
class IteratorBase {
public:
virtual ~IteratorBase() = default;

// 必须实现:解引用、前进、是否相等
virtual int& operator*() = 0;
virtual IteratorBase& operator++() = 0;
virtual bool operator!=(const IteratorBase& other) const = 0;

// clone:用于复制自身(因为我们用的是多态指针)
virtual IteratorBase* clone() const = 0;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ArrayIterator : public IteratorBase {
private:
int* ptr;
public:
ArrayIterator(int* p) : ptr(p) {}

int& operator*() override {return *ptr;}

IteratorBase& operator++() override { ++ptr; return *this;}

bool operator!=(const IteratorBase& other) const override {
const auto* o = dynamic_cast<const ArrayIterator*>(&other);
return o && this->ptr != o->ptr;
}

IteratorBase* clone() const override { return new ArrayIterator(ptr); }
};

自定义迭代器“必须”实现的内容(基本功能)

操作符 功能 示例
operator*() 解引用,访问当前元素 *it
operator++()(前缀) 移动到下一个元素 ++it
operator!=() 判断是否结束 it != end()

只要这 3 个有了,就能支持 C++ 的范围 for 循环:

1
2
3
for (auto it = container.begin(); it != container.end(); ++it) {
std::cout << *it;
}

如果还希望支持 range-based for(现代语法)

则你的容器类还必须提供:

1
2
Iterator begin();
Iterator end();

可以扩充的功能(增强型迭代器)

这些是可选的,但能显著增强迭代器的能力,使它表现得像 STL 中的强大迭代器类型(比如 vectorlistmap)。

操作符 / 方法 作用 用于支持
operator==() 判断两个迭代器是否相等 替代 !=
operator--() 支持向后遍历 双向迭代器
operator++(int) 后缀递增 it++
operator--(int) 后缀递减 it--
operator+=() / operator-= 随机访问前进/后退 N 步 随机访问迭代器
operator+() / operator-() 支持偏移位置计算 it + 3, it - 2
operator[] 支持下标访问 it[5]
difference_type 两迭代器间距离 it2 - it1
iterator_category 告诉 STL:你是哪个种类 与标准算法兼容

dynamic_cast

dynamic_cast是 C++ 中的一种类型转换操作符,主要用于在继承层次结构中进行安全的向下转型(downcasting)或跨转型(crosscasting)。

1
auto other2 = dynamic_cast<const HeapIterator &>(other);

这行代码将other对象动态转换为const HeapIterator&类型的引用。下面详细解释其含义和作用:

  1. 运行时类型检查:与static_cast不同,dynamic_cast会在运行时检查类型转换的有效性。如果转换失败:
    • 对于指针类型,返回nullptr
    • 对于引用类型,抛出std::bad_cast异常
  2. 主要应用场景
    • ==向下转型:从基类指针 / 引用转换为派生类指针 / 引用==
    • 跨转型:在多继承体系中,将指针 / 引用从一个分支转换到另一个分支
  3. 必要条件
    • 基类必须包含至少一个虚函数(即基类必须是多态类型)
    • 转换的目标类型必须是指针或引用

引用类型的 dynamic_cast:

1
const HeapIterator & other2 = dynamic_cast<const HeapIterator &>(other);

这意味着:

  • 如果other确实引用一个HeapIterator对象(或其派生类对象),转换成功
  • 如果other引用的不是HeapIterator类型的对象,会抛出std::bad_cast异常

3.3.3 memtable迭代器

在 LSM 树中,当需要修改数据时,不会直接覆盖旧值,而是:

  1. 更新操作:插入一个新的键值对(相同 key,不同 value)。
  2. 删除操作:插入一个特殊的删除标记(如("k4", "")),称为 Tombstone。

当遍历多个 SkipList 时,如果简单地按顺序拼接结果,会导致:

  • 旧版本可见:新写入的记录可能被旧版本覆盖。
  • 已删除数据出现:标记为删除的键可能仍然被返回。

解决方案:基于优先队列的合并迭代器

使用 优先队列(最小堆)来合并多个 SkipList 的迭代器,确保:

  1. 按 key 排序:每次取出最小的 key。
  2. 按 SkipList ID 排序:相同 key 时,优先取 ID 较小的 SkipList(表示更新的数据)。

工作原理

  1. 初始化:将每个 SkipList 的第一个元素放入堆中。
  2. 迭代过程:
    • 取出堆顶元素(当前最小 key)。
    • 如果该元素是删除标记(Tombstone),则跳过该 key。
    • 如果不是删除标记,则返回该元素作为结果。
    • 从该元素所在的 SkipList 中取下一个元素放入堆中。
  3. 重复:直到堆为空。

处理相同 key 的情况

  • 如果堆中存在多个相同 key 的元素,按 SkipList ID 排序后,ID 较小的元素(新版本)会先被处理。
  • 处理完一个 key 后,立即跳过所有相同 key 的元素(无论是否为删除标记),确保每个 key 只返回一次。

示例分析

假设有两个 SkipList:

1
2
SkipList0 (ID=0): ("k1", "v1") -> ("k4", "") -> ("k5", "v3")
SkipList1 (ID=1): ("k2", "v2") -> ("k3", "v3") -> ("k4", "v4")

迭代器工作流程

  1. 初始堆[("k1", "v1", 0), ("k2", "v2", 1), ("k3", "v3", 1), ("k4", "", 0), ("k4", "v4", 1), ("k5", "v3", 0)]

  2. 处理顺序:

    • 取出堆顶 ("k1", "v1", 0) → 返回 ("k1", "v1")
    • 取出堆顶 ("k2", "v2", 1) → 返回 ("k2", "v2")
    • 取出堆顶 ("k3", "v3", 1) → 返回 ("k3", "v3")
    • 取出堆顶 ("k4", "", 0)(删除标记)→ 跳过所有 k4(包括 ("k4", "v4", 1))。
    • 取出堆顶 ("k5", "v3", 0) → 返回 ("k5", "v3")

最终结果[("k1", "v1"), ("k2", "v2"), ("k3", "v3"), ("k5", "v3")]

3.3.4 SearchIterator

cpp中堆采用优先队列实现,优先队列默认大根堆,memtable要改成小根堆。

std::priority_queue 内部用的是“谁不满足比较器,就把谁放在前面”。所以当比较器是 cmp(a,b)=b < a,就能让小的元素在前面,从而构成小顶堆。

1
2
3
4
5
std::priority_queue<
SearchItem, // 元素类型
std::vector<SearchItem>, // 底层容器类型
std::greater<SearchItem> // 比较函数类型
> items;

greater标准库定义如下:

1
2
3
4
5
6
template<typename T>
struct greater {
bool operator()(const T& a, const T& b) const {
return b < a;
}
};

可以看到标准库的greater仿函数 采用的是小于符号,且**==b<a==** 所以应该重载 <

less标准库定义如下:

1
2
3
4
5
6
template <typename T>
struct less {
bool operator()(const T& a, const T& b) const {
return a < b; // a 是否比 b 小
}
};

我们希望保留最新的数据,旧的数据被丢弃,最新的定义如下:

  • 更大的 tranc_id → 更新事务更晚;
  • 更小的 level → 更靠近写入端(活跃 MemTable);
  • 更小的 idx_ → 更早插入的(table表的序号)。

所以重载< 为:

1
2
3
4
5
6
7
8
9
10
11
12
bool operator<(const SearchItem &a, const SearchItem &b) {
if (a.key_ != b.key_) {
return a.key_ < b.key_;
}
if (a.tranc_id_ > b.tranc_id_) {
return true;
}
if (a.level_ < b.level_) {
return true;
}
return a.idx_ < b.idx_;
}

3.3.5 HeapIterator

3.3.6 MemTableIterator

MemTableIterator没有固定实现,是利用的HeapIterator迭代器,封装到类的成员函数。

1.emplace_back和push_back的区别

emplace_back 是 C++ 标准库中 std::vector 容器的一个成员函数,用于在容器的尾部直接构造一个新元素。它是 C++11 引入的特性,相比传统的 push_back,它能更高效地添加元素,尤其是对于需要复杂构造的对象。

基本功能

emplace_back 的作用是在 vector 的末尾就地构造一个元素,无需进行额外的拷贝或移动操作。

语法

1
2
template< class... Args >
void emplace_back( Args&&... args );
  • Args&&... args:传递给元素构造函数的参数包(可变参数)
  • 这些参数会被转发给元素类型的构造函数

push_back 的对比

push_back 的两种重载形式

1
2
3
4
5
// 重载1:接受左值引用(拷贝)
void push_back( const T& value );

// 重载2:接受右值引用(移动)
void push_back( T&& value );

核心区别

操作 步骤
push_back(obj) 1. 创建一个 obj 的副本 2. 将副本移动(或拷贝)到 vector
emplace_back(...) 1. 直接在 vector 的内存空间中调用构造函数 2. 使用参数 ... 初始化对象

示例对比

假设我们有一个类 Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
public:
Person(const std::string& name, int age) : name_(name), age_(age) {
std::cout << "Constructed with name: " << name_ << std::endl;
}
// 拷贝构造函数
Person(const Person& other) : name_(other.name_), age_(other.age_) {
std::cout << "Copied" << std::endl;
}
// 移动构造函数
Person(Person&& other) noexcept : name_(std::move(other.name_)), age_(other.age_) {
std::cout << "Moved" << std::endl;
}
private:
std::string name_;
int age_;
};

使用 push_back

1
2
3
4
5
6
std::vector<Person> people;
people.push_back(Person("Alice", 25)); // 临时对象 -> 移动构造(可能被优化)
people.push_back({"Bob", 30}); // C++17 支持的列表初始化 -> 移动构造

Person alice("Alice", 25);
people.push_back(alice); // 拷贝构造

使用 emplace_back

1
2
3
std::vector<Person> people;
people.emplace_back("Alice", 25); // 直接构造,无需拷贝或移动
people.emplace_back("Bob", 30); // 同样直接构造

emplace_back 的优势

效率更高

  • 避免了临时对象的创建和移动 / 拷贝操作
  • 对于构造代价高的对象(如包含动态资源的类),性能提升显著

支持无法拷贝 / 移动的类型

可以直接构造那些删除了拷贝 / 移动构造函数的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class NonCopyable {
public:
NonCopyable(int value) : value_(value) {}
NonCopyable(const NonCopyable&) = delete; // 删除拷贝构造函数
private:
int value_;
};

std::vector<NonCopyable> vec;
// 错误:无法使用 push_back
// vec.push_back(NonCopyable(42));

// 正确:可以使用 emplace_back
vec.emplace_back(42);

语法更简洁

对于需要多个参数构造的对象,无需显式创建临时对象:

1
2
// 无需写 people.push_back(Person("Alice", 25));
people.emplace_back("Alice", 25);

使用场景

  • 添加复杂对象:当元素构造需要多个参数时
  • 性能敏感的代码:避免不必要的拷贝 / 移动操作
  • 添加不可拷贝的对象:如包含锁、文件句柄等资源的对象

注意事项

  1. 内存重新分配:如果 vector 需要扩容,emplace_back 仍可能触发元素的移动(与 push_back 相同)。

  2. 构造函数匹配:确保传递给 emplace_back 的参数能正确匹配元素类型的构造函数,否则会导致编译错误:

    1
    2
    std::vector<int> nums;
    nums.emplace_back("hello"); // 错误:无法将 const char* 转换为 int
  3. 与初始化列表的区别

    1
    2
    3
    4
    5
    // 使用初始化列表(创建临时对象,再移动)
    people.push_back({"Alice", 25}); // 调用 push_back(Person({"Alice", 25}))

    // 直接构造(更高效)
    people.emplace_back("Alice", 25);

3.4 补充知识

3.4.1 运算符重载的实现方式

  1. 成员函数:作为结构体的成员函数
1
2
3
4
5
6
7
8
9
10
11
12
struct SearchItem {
// 其他成员...

bool operator<(const SearchItem &other) const {
// 定义小于比较的逻辑
// 例如:先比较key,再比较其他字段
if (key_ != other.key_) return key_ < other.key_;
return tranc_id_ < other.tranc_id_; // 或者其他比较逻辑
}

// 其他运算符重载...
};
  1. 非成员函数:作为全局函数或命名空间内的函数
1
2
3
4
5
6
7
8
9
struct SearchItem {
// 结构体定义...
};

// 全局函数形式的运算符重载
bool operator<(const SearchItem &a, const SearchItem &b) {
// 实现比较逻辑
return a.key_ < b.key_;
}

3.4.2 std::pair 简介

std::pair 是 C++ 标准库中的一个模板类,用于将两个值(可能是不同类型)组合成一个单元。它不是传统意义上的容器(如 vectorlist),而是一个简单的数据结构,主要用于存储一对相关的值。

1
2
3
4
5
6
7
#include <utility>  // 必须包含的头文件

template <class T1, class T2>
struct pair {
T1 first; // 第一个成员
T2 second; // 第二个成员
};

主要用途

  • 在函数中返回多个值
  • 在关联容器(如 mapunordered_map)中存储键值对
  • 临时组合两个值,避免创建专门的结构体

创建与初始化

  1. 直接构造
1
2
std::pair<std::string, int> p1("Alice", 25);  // 显式类型
auto p2 = std::pair("Bob", 30); // C++17 模板参数推导
  1. 使用 make_pair
1
auto p3 = std::make_pair("Charlie", 35);  // 自动推导类型
  1. 列表初始化(C++11 及以后)
1
2
std::pair<std::string, int> p4{"David", 40};  // 统一初始化语法
auto p5 = std::pair{"Eve", 45}; // C++17 类型推导

访问与修改成员

  1. 通过 .first.second
1
2
3
4
5
6
7
std::pair<std::string, int> person("Alice", 25);

std::cout << person.first << ", " << person.second << std::endl; // 输出:Alice, 25

person.first = "Bob";
person.second++;
std::cout << person.first << ", " << person.second << std::endl; // 输出:Bob, 26
  1. 结构化绑定(C++17 及以后)
1
2
auto [name, age] = person;  // 直接解包到变量
std::cout << name << ", " << age << std::endl; // 输出:Bob, 26

常用操作

  1. 比较运算符

pair 支持所有比较运算符(==, !=, <, >, <=, >=),按字典序比较:

1
2
3
4
5
6
std::pair<int, char> p1(1, 'a');
std::pair<int, char> p2(1, 'b');
std::pair<int, char> p3(2, 'a');

bool b1 = p1 == p2; // false(first相同,但second不同)
bool b2 = p1 < p3; // true(p1.first < p3.first)
  1. 交换内容
1
2
3
4
std::pair<int, std::string> p1(1, "one");
std::pair<int, std::string> p2(2, "two");

p1.swap(p2); // 交换 p1 和 p2 的内容
  1. 作为容器的元素
1
2
3
4
5
6
7
8
9
10
#include <vector>

std::vector<std::pair<std::string, int>> people;
people.push_back({"Alice", 25});
people.push_back({"Bob", 30});

// 遍历
for (const auto& [name, age] : people) {
std::cout << name << ", " << age << std::endl;
}
  1. 关联容器的底层实现

std::mapstd::unordered_map 的元素类型是 std::pair<const Key, T>

1
2
3
4
5
6
7
8
9
10
#include <map>

std::map<std::string, int> ages;
ages["Alice"] = 25;
ages["Bob"] = 30;

// 迭代器返回 pair<const string, int>
for (const auto& pair : ages) {
std::cout << pair.first << ", " << pair.second << std::endl;
}

3.4.2 make_optional

返回值解析:std::optional<std::pair<SkipListIterator, SkipListIterator>>

这个返回值类型组合了三个 C++ 特性:std::optionalstd::pair迭代器SkipListIterator),其含义是:

函数可能返回一对迭代器(表示一个范围),也可能没有有效结果(用 std::nullopt 表示)。这在查找操作中很常见,例如:

  • 当查询成功时,返回一个包含起始迭代器结束迭代器pair
  • 当查询失败时,返回 std::nullopt(空值)

std::optional 是 C++17 引入的一个模板类,用于表示一个可能存在的值。它是对 “值可能缺失” 场景的类型安全封装,替代了传统的空指针或特殊值标记(如 -1 表示失败)。std::make_optional 则是一个辅助函数,用于便捷地创建 std::optional 对象。

一、std::optional 的基本概念

  1. 核心作用
  • 显式表示一个值可能存在或不存在
  • 避免使用空指针(nullptr)或魔数(如 -1
  • 提供类型安全的空值处理机制
  1. 简单示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <optional>
#include <string>

// 返回可能存在的字符串
std::optional<std::string> getUserName() {
if (userExists()) {
return "Alice"; // 返回有值的 optional
}
return std::nullopt; // 返回空的 optional
}

// 使用 optional
auto name = getUserName();
if (name.has_value()) {
std::cout << "Name: " << name.value() << std::endl;
} else {
std::cout << "User not found" << std::endl;
}

二、std::optional 的常用操作

  1. 创建 optional 对象
1
2
3
4
5
6
7
// 1. 直接构造
std::optional<int> opt1 = 42; // 有值的 optional
std::optional<int> opt2 = std::nullopt; // 空的 optional

// 2. 使用 std::make_optional(推荐)
auto opt3 = std::make_optional(3.14); // 自动推导类型为 optional<double>
auto opt4 = std::make_optional<std::vector<int>>({1, 2, 3});
  1. 检查是否有值
1
2
if (opt.has_value()) { /* ... */ }  // 显式检查
if (opt) { /* ... */ } // 隐式转换为 bool
  1. 获取值
1
2
3
4
5
6
// 1. 安全获取(带检查)
int value1 = opt.value(); // 若无值,抛出 std::bad_optional_access 异常
int value2 = opt.value_or(0); // 若无值,返回默认值 0

// 2. 不安全获取(需自行确保有值)
int value3 = *opt; // 等价于 opt.value(),但无检查
  1. 修改值
1
2
3
opt = 100;            // 赋值(若原无值,创建值;若原有值,替换值)
opt.reset(); // 清空值(变为无值状态)
opt.emplace(200); // 就地构造值(覆盖原有值)

三、std::make_optional 的作用

std::make_optional 是一个辅助函数,用于自动推导并创建 std::optional 对象,其优势在于:

  1. 自动类型推导:无需显式指定模板参数
  2. 避免拷贝:使用完美转发,效率更高
  3. 统一语法:与 std::make_pairstd::make_shared 等保持一致

示例对比

1
2
3
4
5
6
// 不使用 make_optional
std::optional<std::pair<int, std::string>> opt1 =
std::optional<std::pair<int, std::string>>(std::pair(42, "hello"));

// 使用 make_optional(更简洁)
auto opt2 = std::make_optional(std::pair(42, "hello"));

四、为什么需要 std::optional

  1. 替代空指针的问题

传统使用指针表示可能缺失的值:

1
2
3
4
5
6
7
8
9
10
// 危险:需手动检查 nullptr
std::string* getName() {
return userExists() ? new std::string("Alice") : nullptr;
}

// 调用者必须检查
if (auto name = getName(); name != nullptr) {
std::cout << *name << std::endl;
delete name; // 还需手动释放内存!
}
  1. 替代魔数的问题

使用特殊值(如 -1)表示失败:

1
2
3
4
5
6
7
8
9
// 不直观:-1 是魔法数字,语义不明确
int findIndex(const std::string& s) {
if (s.empty()) return -1;
// ...
}

// 调用者必须记住 -1 的含义
int idx = findIndex("test");
if (idx != -1) { /* ... */ }
  1. std::optional 的优势
  • 类型安全:编译器强制处理空值情况
  • 语义明确:从类型定义即可看出 “可能无值”
  • 资源安全:无需手动管理内存(如 new/delete

五、进阶用法

  1. 与函数式编程结合
1
2
3
4
5
6
// 链式调用(C++20 及以后)
std::optional<int> opt = 42;
auto result = opt.transform([](int x) { return x * 2; })
.and_then([](int x) -> std::optional<int> {
return x > 100 ? std::nullopt : std::make_optional(x);
});
  1. 作为函数参数
1
2
3
4
5
6
7
8
9
10
void processValue(std::optional<int> opt) {
if (opt) {
std::cout << "Value: " << *opt << std::endl;
} else {
std::cout << "No value" << std::endl;
}
}

processValue(42); // 传递有值的 optional
processValue(std::nullopt); // 传递无值的 optional

六、注意事项

  1. 性能开销
    • std::optional 通常使用一个字节的标志位来表示有无值,因此空间开销极小
    • 但对于本身就是可空的类型(如指针),使用 optional 可能增加不必要的开销
  2. 兼容性
    • C++17 及以后版本直接支持 std::optional
    • C++14 及以前可使用 std::experimental::optional(需包含 <experimental/optional>
    • 若需兼容更旧的编译器,可使用第三方库(如 Boost.Optional)
  3. 避免过度使用
    • 仅在 “值缺失是合理情况” 时使用 optional
    • 不要用 optional 替代正常的错误处理(如异常或错误码)

四、SST

4.1 Block

4.1.1 编码和解码

分为两个步骤。编码(encoded)和解码(decoded)。

编码就是构建block,编码单个类实例形成一段字节数组(Block)。

解码就是读取一段字节数组,返回一个类实例,目的是将Block读取到内存,为上层组件提供查询功能。

4.1.2 add_entry

非强制写:

判断添加后的block大小和Blokc的capacity容量比较,如果大于,则返回false,添加条目失败。

添加后大小=cur_size +key_size + value_size+ 3*uint16_t (键大小,值大小,偏移量)+uint64_t (事务id)

4.1.3 直接赋值和返回this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Block : public std::enable_shared_from_this<Block> {
public:
std::shared_ptr<Block> getSharedPtr() {
return shared_from_this(); // 正确方式
}
};

// 正确用法
auto block = std::make_shared<Block>(); // 引用计数 = 1
auto another = block->getSharedPtr(); // 引用计数 = 2(共享同一计数)

// 离开作用域时:
// 1. another 析构,引用计数减为 1
// 2. block 析构,引用计数归零,安全释放对象

当你已经持有一个 shared_ptr<Block> 对象(如 block)时,直接赋值是最简单的方式,这种方式完全正确且高效,无需额外的 getSharedPtr() 方法。

1
2
auto block = std::make_shared<Block>();  // 外部创建的 shared_ptr
auto another = block; // 直接赋值,引用计数+1

当你需要从对象内部获取自身的 shared_ptr 时,直接赋值无法实现,必须通过 shared_from_this()

1
2
3
4
5
6
7
8
9
10
class Block : public std::enable_shared_from_this<Block> {
public:
void registerWith(EventLoop& loop) {
// 错误:无法从 this 直接构造 shared_ptr
// loop.registerCallback(this); // 编译错误

// 正确:通过 shared_from_this() 获取自身的 shared_ptr
loop.registerCallback(shared_from_this());
}
};

关键区别

  • 直接赋值:外部已经持有 shared_ptr,只需复制该指针。
  • getSharedPtr():在对象内部(如成员函数),需要将 this 转换为 shared_ptr

为什么不能在对象内部直接使用 this

  1. 类型不匹配

this 是裸指针(Block*),而 registerCallback 期望的是 shared_ptr<Block>

1
2
// 错误:无法将 Block* 隐式转换为 shared_ptr<Block>
loop.registerCallback(this); // 编译错误
  1. 生命周期管理风险

即使强制转换,也会导致双重释放:

1
2
// 致命错误:创建了独立的 shared_ptr,与外部的 shared_ptr 无关
loop.registerCallback(std::shared_ptr<Block>(this)); // 双重释放!

getSharedPtr() 的典型应用场景

  1. 注册回调函数

将自身的 shared_ptr 传递给其他组件,确保回调时对象存活:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class EventLoop {
public:
void registerCallback(std::shared_ptr<Handler> handler) {
callbacks_.push_back(handler); // 持有 shared_ptr 以延长生命周期
}
};

class Handler : public std::enable_shared_from_this<Handler> {
public:
void startListening(EventLoop& loop) {
// 通过 shared_from_this() 安全传递自身
loop.registerCallback(shared_from_this());
}
};

// 使用示例
auto handler = std::make_shared<Handler>();
handler->startListening(loop); // 确保 loop 持有 handler 的引用
  1. 链式调用返回自身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Builder : public std::enable_shared_from_this<Builder> {
public:
std::shared_ptr<Builder> withConfig(const Config& cfg) {
// 配置操作...
return shared_from_this(); // 支持链式调用
}

std::shared_ptr<Product> build() {
// 构建产品...
return result;
}
};

// 使用示例
auto product = Builder::create()
->withConfig(cfg1)
->withConfig(cfg2)
->build();

4.2 SST Builder

4.3 file

4.3.1 std_file

1
std::fstream file_

std::fstream 是 C++ 标准库中用于文件输入输出的类,它继承自std::iostream,结合了std::ifstream(输入文件流) 和std::ofstream(输出文件流) 的功能。主要特点:

  1. 模式标志:打开文件时需指定模式:

    • std::ios::in - 读模式
    • std::ios::out - 写模式
    • std::ios::app - 追加模式
    • std::ios::trunc - 截断文件
    • std::ios::binary - 二进制模式
  2. 常用操作

    1
    2
    3
    4
    5
    6
    7
    file_.open("example.txt", std::ios::in | std::ios::out);
    if(file_.is_open()) {
    file_ << "写入数据"; // 写操作
    std::string line;
    std::getline(file_, line); // 读操作
    file_.close();
    }
  3. 状态检查

    1
    2
    3
    if(file_.good()) // 所有操作正常
    if(file_.eof()) // 到达文件末尾
    if(file_.fail()) // 操作失败
  4. 文件定位

    1
    2
    file_.seekg(0, std::ios::beg); // 移动读指针到文件开头
    file_.seekp(10, std::ios::cur); // 移动写指针到当前位置+10
1
std::filesystem::path filename_

std::filesystem::path 是 C++17 引入的文件系统库的核心类,用于表示和操作文件路径。主要特点:

  1. 平台无关性

    • 自动处理不同操作系统的路径分隔符 (Windows 的\和 Unix 的/)
    • 支持 Unicode 路径名
  2. 路径操作

    1
    2
    3
    filename_ = "data";
    filename_ /= "subdir"; // 追加路径组件
    filename_ /= "file.txt"; // 结果: "data/subdir/file.txt"
  3. 路径分解

    1
    2
    3
    4
    std::cout << filename_.parent_path();  // "data/subdir"
    std::cout << filename_.filename(); // "file.txt"
    std::cout << filename_.stem(); // "file"
    std::cout << filename_.extension(); // ".txt"
  4. 查询操作

    1
    2
    if(filename_.is_absolute()) // 是否绝对路径
    std::cout << filename_.string(); // 转换为字符串
  5. fstream结合

    1
    file_.open(filename_, std::ios::in);

std::fstream file_

std::fstream 是 C++ 标准库中用于文件输入输出的类,它继承自std::iostream,结合了std::ifstream(输入文件流) 和std::ofstream(输出文件流) 的功能。主要特点:

  1. 模式标志:打开文件时需指定模式:

    • std::ios::in - 读模式
    • std::ios::out - 写模式
    • std::ios::app - 追加模式
    • std::ios::trunc - 截断文件
    • std::ios::binary - 二进制模式
  2. 常用操作

    1
    2
    3
    4
    5
    6
    7
    file_.open("example.txt", std::ios::in | std::ios::out);
    if(file_.is_open()) {
    file_ << "写入数据"; // 写操作
    std::string line;
    std::getline(file_, line); // 读操作
    file_.close();
    }
  3. 状态检查

    1
    2
    3
    if(file_.good()) // 所有操作正常
    if(file_.eof()) // 到达文件末尾
    if(file_.fail()) // 操作失败
  4. 文件定位

    1
    2
    file_.seekg(0, std::ios::beg); // 移动读指针到文件开头
    file_.seekp(10, std::ios::cur); // 移动写指针到当前位置+10

std::filesystem::path filename_

std::filesystem::path 是 C++17 引入的文件系统库的核心类,用于表示和操作文件路径。主要特点:

  1. 平台无关性

    • 自动处理不同操作系统的路径分隔符 (Windows 的\和 Unix 的/)
    • 支持 Unicode 路径名
  2. 路径操作

    1
    2
    3
    filename_ = "data";
    filename_ /= "subdir"; // 追加路径组件
    filename_ /= "file.txt"; // 结果: "data/subdir/file.txt"
  3. 路径分解

    1
    2
    3
    4
    std::cout << filename_.parent_path();  // "data/subdir"
    std::cout << filename_.filename(); // "file.txt"
    std::cout << filename_.stem(); // "file"
    std::cout << filename_.extension(); // ".txt"
  4. 查询操作

    1
    2
    if(filename_.is_absolute()) // 是否绝对路径
    std::cout << filename_.string(); // 转换为字符串
  5. fstream结合

    1
    file_.open(filename_, std::ios::in);

最佳实践建议

  1. 异常安全

    1
    2
    3
    4
    5
    6
    7
    try {
    if(std::filesystem::exists(filename_)) {
    // 文件存在处理逻辑
    }
    } catch(const std::filesystem::filesystem_error& e) {
    std::cerr << "文件系统错误: " << e.what() << '\n';
    }
  2. RAII 特性
    std::fstream 遵循 RAII 原则,对象销毁时自动关闭文件,无需手动调用close()

  3. 错误处理

    1
    2
    3
    4
    file_.open(filename_, std::ios::out);
    if(!file_.is_open()) {
    std::cerr << "无法打开文件: " << filename_.string() << '\n';
    }
  4. 路径规范

    1
    auto canonical_path = std::filesystem::canonical(filename_);
1
2
3
4
5
6
7
8
std::vector<uint8_t> StdFile::read(size_t offset, size_t length) {
std::vector<uint8_t> buf(length);
file_.seekg(offset, std::ios::beg);
if (!file_.read(reinterpret_cast<char *>(buf.data()), length)) {
throw std::runtime_error("Failed to read from file");
}
return buf;
}
  1. 定位文件指针

    1
    file_.seekg(offset, std::ios::beg);
    • seekgstd::fstream的成员函数,用于设置输入(读取)指针位置
    • offset:偏移量
    • std::ios::beg:参考点为文件开头
  2. 读取数据

    1
    file_.read(reinterpret_cast<char*>(buf.data()), length);
    • read函数从文件流读取length字节到指定内存地址
    • buf.data()返回指向vector内部数组的指针
    • reinterpret_cast<char*>uint8_t*转换为char*以匹配read参数要求

4.3.2 MmapFile

这个 MmapFile 类是对操作系统提供的 内存映射文件机制(Memory-Mapped File, mmap) 的封装,主要目的是:将文件直接映射到内存中,从而能像操作内存一样高效读写文件内容。

特点 StdFile MmapFile
操作方式 使用 std::fstream(传统文件流) 使用 mmap 系统调用映射文件到内存
读写方式 每次读取或写入都调用文件 API 可以直接访问内存区域,更高效
使用场景 适合普通文件读写 适合频繁访问大文件或性能要求高的场景,如数据库、缓存系统等

类成员详解

成员变量(私有)

  • int fd_: 文件描述符,用于 Unix/Linux 系统的底层文件操作(如 open())。
  • void *mapped_data_: 文件被映射到的内存地址。
  • size_t file_size_: 文件的大小。
  • std::string filename_: 保存文件名。

成员函数(公开)

MmapFile()构造函数,初始化成员变量。

~MmapFile()析构函数,自动关闭文件和解除映射,防止资源泄漏。


文件操作 API

bool open(const std::string &filename, bool create = false);

  • 打开文件并映射到内存,create = true 时如果文件不存在则创建。

bool create(const std::string &filename, std::vector<uint8_t> &buf);

  • 创建新文件,并用给定的数据初始化文件内容,同时映射到内存。

void close();

  • 关闭文件,并取消内存映射。

数据操作 API

size_t size() const

  • 返回文件大小。

bool write(size_t offset, const void *data, size_t size);

  • 把数据写入映射的内存区域。非常快,相当于直接修改内存。

std::vector<uint8_t> read(size_t offset, size_t length);

  • 从映射的内存区域中读取数据,不需要文件系统调用。

bool sync();

  • 将映射内存中的更改同步回磁盘(msync() 系统调用),确保数据持久化。

禁止拷贝

1
2
MmapFile(const MmapFile &) = delete;
MmapFile &operator=(const MmapFile &) = delete;
  • 防止不小心复制对象,导致两个对象指向相同的文件映射区域,造成潜在的内存和资源错误。

4.3.3 FileObj

本质理解:这是一个“文件工具类”或“文件对象封装类” ,它包装了一个 StdFile(通过智能指针管理),并对外提供了一组统一的接口:读取、写入、同步、追加、删除等。这使得上层逻辑用起来更加简单、统一、现代(移动语义、安全)

类内部组成详解

成员变量

1
2
std::unique_ptr<StdFile> m_file; // 指向 StdFile 的智能指针(用来操作文件)
size_t m_size; // 文件当前大小(手动维护)
  • 用智能指针管理文件对象,自动析构,防止内存泄漏。
  • m_size 保存当前文件大小,用于写入/追加等操作中方便计算偏移。

类方法一览(配合前面的 StdFile

文件生命周期管理

方法 说明
FileObj() / ~FileObj() 构造/析构函数
FileObj(const FileObj&) = delete 禁止拷贝(避免误用共享文件资源)
FileObj(FileObj &&other) 允许移动
operator=(FileObj&&) 移动赋值

文件大小接口

1
2
size_t size() const;
void set_size(size_t size);
  • size() 返回当前文件大小。
  • set_size() 设置文件大小(用于手动更新,比如 append 后更新)。

文件创建 / 打开 / 删除

1
2
3
static FileObj create_and_write(const std::string &path, std::vector<uint8_t> buf);
static FileObj open(const std::string &path, bool create);
void del_file();
  • 静态函数:直接创建对象并打开或创建文件,写入初始化数据。
  • del_file() 删除底层文件(其实是调用 StdFile::remove())。

读取接口

1
2
3
4
5
std::vector<uint8_t> read_to_slice(size_t offset, size_t length);
uint8_t read_uint8(size_t offset);
uint16_t read_uint16(size_t offset);
uint32_t read_uint32(size_t offset);
uint64_t read_uint64(size_t offset);

这些方法从文件中指定 offset 读取数据(整段或者具体类型),是典型的底层数据格式解析场景。

比如你存储了一个结构体到文件,现在就能逐个字段读取回来。


写入接口

1
2
bool write(size_t offset, std::vector<uint8_t> &buf);
bool append(std::vector<uint8_t> &buf);
  • write(...):写入到指定位置(偏移量)
  • append(...):追加数据到底部,自动更新 m_size,用作日志、块追加场景。

同步接口

1
bool sync();
  • 调用底层 StdFile::sync(),将数据刷新到磁盘,确保不会因为内核缓存丢失。

总结

功能 对应
资源管理 unique_ptr<StdFile> 自动关闭文件、避免拷贝
文件创建、删除 create_and_write()del_file()
文件读写 读取各种类型(uint8_t / uint16_t / …)
文件同步 sync() 保证持久化
简洁封装 用更高层的接口屏蔽 StdFile 复杂逻辑

4.4 补充知识

4.4.1 size_t和uint8_t

特性 size_t uint8_t
类型本质 无符号整数,用于表示对象大小或内存空间 无符号 8 位整数(精确宽度类型)
头文件 <cstddef>, <iostream>, 等 <cstdint>
常见用途 数组索引、容器大小、内存操作 字节数据、二进制编码、位操作
平台相关性 宽度可变(32/64 位系统) 固定 8 位(跨平台一致)

4.4.2 vector::data()

  • vector::data() 是 C++ 标准库提供的成员函数,返回指向向量底层数组裸指针int*)。
  • 该指针保证指向连续内存,与 &vector[0] 等价。

4.4.3 std::enable_shared_from_this

std::enable_shared_from_this 是 C++ 标准库中的一个模板类(定义于 <memory>),用于让类的成员函数能够安全地获取指向当前对象的 std::shared_ptr。这在需要将当前对象的引用传递给其他函数或对象时非常有用。

1
2
3
4
class Block : public std::enable_shared_from_this<Block> 
{
//...
}

如果一个类直接在成员函数中返回 this 指针,外部代码可能会创建多个独立的 std::shared_ptr 管理同一个对象,导致对象被多次删除(内存泄漏或崩溃):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Block {
public:
Block* getRawPtr() { return this; } // 危险!

// 错误示例:可能导致多个独立的 shared_ptr 管理同一对象
std::shared_ptr<Block> getSharedPtr() {
return std::shared_ptr<Block>(this); // 严重错误!
}
};

// 错误用法
auto ptr1 = std::make_shared<Block>(); //ptr1 管理对象,引用计数 = 1
auto ptr2 = ptr1->getSharedPtr(); //ptr2 独立管理同一对象,引用计数 = 1(与 ptr1 无关)
// 当 ptr1 和 ptr2 析构时,对象会被删除两次!

当使用 std::make_shared<Block>() 创建智能指针时:

  • 同时分配对象内存和控制块
  • 控制块中的引用计数初始化为 1

当通过 shared_ptr<Block>(this) 从原始指针创建智能指针时:

  • 仅分配新的控制块(对象内存已存在)
  • 新控制块的引用计数初始化为 1
  • 两个控制块相互独立,不知道对方的存在

正确方式:通过 shared_from_this()

std::enable_shared_from_this 提供了 shared_from_this() 成员函数,它返回一个与已有 std::shared_ptr 共享所有权的新指针:

1
2
3
4
5
6
7
8
9
10
11
class Block : public std::enable_shared_from_this<Block> {
public:
std::shared_ptr<Block> getSharedPtr() {
return shared_from_this(); // 安全:返回已存在的 shared_ptr 的副本
}
};

// 正确用法
auto ptr1 = std::make_shared<Block>();//引用计数初始化为 1
auto ptr2 = ptr1->getSharedPtr(); // ptr2 与 ptr1 共享所有权 引用计增加为 2
// 当 ptr1 和 ptr2 都析构时,对象仅被删除一次

std::enable_shared_from_this 的工作原理

1. 内部机制

  • 当一个类继承 std::enable_shared_from_this<T> 时,基类会暗中跟踪管理该对象的 std::shared_ptr
  • 第一次创建 std::shared_ptr<Block> 时,智能指针会自动注册到基类的隐藏数据结构中。
  • 调用 shared_from_this() 时,直接从这个隐藏数据结构中获取已存在的 std::shared_ptr,避免创建新的独立指针。

2. 继承要求

  • 必须通过 public 继承 std::enable_shared_from_this
  • 类的模板参数必须是该类本身(如 class Block : public std::enable_shared_from_this<Block>)。

使用场景

  1. 向异步操作传递自身引用
1
2
3
4
5
6
7
class Block : public std::enable_shared_from_this<Block> {
public:
void asyncProcess() {
// 将自身的 shared_ptr 传递给异步操作,确保操作期间对象不被销毁
someAsyncFunction(shared_from_this());
}
};
  1. 注册回调函数
1
2
3
4
5
6
7
8
9
class Block : public std::enable_shared_from_this<Block> {
public:
void registerCallback() {
// 注册自身的 shared_ptr 作为回调,确保回调执行时对象仍然存在
eventManager.registerHandler([self = shared_from_this()]() {
self->onEvent();
});
}
};
  1. 返回自身的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Block : public std::enable_shared_from_this<Block> {
public:
std::shared_ptr<Block> clone() {
auto newBlock = std::make_shared<Block>(*this);
return newBlock;
}

void addChild(std::shared_ptr<Block> child) {
children.push_back(child);
child->parent = shared_from_this(); // 安全地设置父节点
}

private:
std::vector<std::shared_ptr<Block>> children;
std::weak_ptr<Block> parent; // 使用 weak_ptr 避免循环引用
};

注意事项

  1. 必须通过 shared_ptr 创建对象
  • 只有当对象已经被 std::shared_ptr 管理时,调用 shared_from_this() 才是安全的。
  • 如果对象是通过 new 直接创建的(或栈上创建),调用 shared_from_this() 会抛出 std::bad_weak_ptr 异常:
1
2
3
4
5
Block* rawPtr = new Block();
// rawPtr->shared_from_this(); // 错误:对象不是由 shared_ptr 管理

auto sharedPtr = std::make_shared<Block>();
sharedPtr->shared_from_this(); // 正确
  1. 构造函数和析构函数中禁止使用
  • 在构造函数中,对象尚未被 std::shared_ptr 管理,调用 shared_from_this() 会失败。
  • 在析构函数中,std::shared_ptr 可能已经释放了对象的所有权,调用也会失败。
  1. 避免循环引用
  • 如果 AB 互相持有对方的 shared_ptr,会导致内存泄漏。此时应使用 std::weak_ptr 打破循环:
1
2
3
4
5
6
7
8
9
class A : public std::enable_shared_from_this<A> {
public:
std::weak_ptr<B> bPtr; // 使用 weak_ptr 避免循环引用
};

class B : public std::enable_shared_from_this<B> {
public:
std::weak_ptr<A> aPtr; // 使用 weak_ptr 避免循环引用
};
操作方式 底层实现 引用计数变化 控制块数量
ptr2 = ptr1 拷贝ptr1的控制块指针,引用计数加 1。 1→2 1 个
ptr2 = shared_from_this() 通过类内部存储的weak_ptr创建shared_ptr,引用计数加 1。 1→2 1 个
ptr2 = std::shared_ptr<Block>(this) 错误方式:创建新的控制块,与原ptr1的控制块无关,引用计数独立为 1。 各为 1 2 个

4.4.4 hash

1
2
3
uint32_t compute_hash =std::hash<std:: string_view>{}(
std::string_view(reinterpret_cast<const char *>(encoded.data()),encoded.size()-sizeof(uint32_t))
);

std::hash 是 C++ 标准库(<functional> 头文件)中用于生成哈希值的函数对象模板,广泛应用于哈希表(如 unordered_mapunordered_set)、哈希集合等数据结构中。它通过将输入数据映射为一个 size_t 类型的整数值(哈希值),实现高效的数据检索和存储。

核心特性

  1. 函数对象模板
    std::hash<T> 是针对类型 T 定义的模板,不同类型需要特化以提供哈希计算逻辑。例如:
    • std::hash<int>:整数哈希
    • std::hash<std::string>:字符串哈希
    • std::hash<double>:浮点数哈希(需注意精度问题)
  2. 返回值类型
    始终返回 size_t 类型(无符号整数,通常与平台指针长度一致,如 64 位系统为 64 位)。
  3. 哈希特性
    • 确定性:相同输入必定生成相同哈希值。
    • 高效性:计算复杂度通常为 O (1) 或 O (n)(n 为数据长度,如字符串)。
    • 分布性:理想情况下,不同输入的哈希值应均匀分布,减少哈希冲突。

标准库预定义的特化

C++ 标准库为常见类型提供了 std::hash 的特化实现:

类型 哈希计算方式
bool, char, int, long 等基础类型 直接将数值转换为 size_t(如 int 的值直接作为哈希值)。
float, double 将二进制位 reinterpret 为 uint32_tuint64_t 后计算哈希。
std::string 对字符串每个字符的 ASCII 码进行迭代计算(通常使用类似 DJB2 或 FNV 算法)。
std::wstring, std::u16string, std::u32string 类似 std::string,按字符类型处理。
std::shared_ptr<T>, std::unique_ptr<T> 基于指针地址计算哈希。
std::pair<T1, T2>, std::tuple<T...> 组合各个元素的哈希值(如 hash(pair) = hash(first) ^ (hash(second) << 1))。

使用示例

  1. 基础类型哈希

    1
    2
    3
    4
    5
    std::hash<int> hash_int;
    std::cout << hash_int(42) << std::endl; // 输出 42 对应的哈希值

    std::hash<std::string> hash_str;
    std::cout << hash_str("hello") << std::endl; // 输出字符串的哈希值
  2. 在容器中使用

    1
    2
    3
    std::unordered_map<std::string, int> map;
    // 内部自动使用 std::hash<std::string> 计算键的哈希值
    map["key"] = 100;
  3. 自定义类型的哈希
    若要对自定义类型(如结构体)使用哈希,需特化 std::hash

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct Person {
    std::string name;
    int age;
    };

    // 为 Person 特化 std::hash
    namespace std {
    template<>
    struct hash<Person> {
    size_t operator()(const Person& p) const {
    return std::hash<std::string>{}(p.name) ^ (std::hash<int>{}(p.age) << 1);
    }
    };
    }

    // 使用示例
    std::unordered_set<Person> people;

注意事项

  1. 哈希冲突
    不同输入可能生成相同哈希值(冲突),标准库容器会通过链表或开放寻址法处理冲突,但良好的哈希函数可减少冲突概率。
  2. 浮点数哈希的不确定性
    • NaN 的哈希值未定义,不同 NaN 可能生成不同哈希值。
    • 精度损失可能导致不同数值生成相同哈希值(如 1.01.0000000001)。
  3. 自定义哈希的要求
    自定义类型的哈希函数需满足:
    • a == b,则 hash(a) == hash(b)(一致性)。
    • 尽量保证不同值的哈希值分布均匀。
  4. C++17 新增特性
    • std::hash<std::string_view>:高效处理字符串视图的哈希。
    • std::hash 支持更多标准类型(如 std::optionalstd::variant)。

与其他哈希函数的对比

  • boost::hash 的区别
    boost::hash 提供更强大的组合哈希功能(如直接支持元组),而 std::hash 是标准库的一部分,兼容性更好。
  • 与具体哈希算法(如 MD5、SHA-1)的区别
    std::hash 侧重于高效映射(速度优先),而 MD5 等加密哈希算法侧重抗碰撞性(安全性优先),两者应用场景不同。

4.4.5 std::string_view

  • 轻量级字符串引用:不拥有字符串数据,仅指向现有字符数组(如 std::stringconst char*)。

  • 零拷贝:避免字符串复制,提升性能。

  • 用法示例:

    1
    2
    std::string str = "hello world";
    std::string_view view(str.data(), 5); // 指向 "hello"

std::hash<std::string_view> 的优势

  1. 高效性
  • 直接处理内存区域:无需构造完整的 std::string,直接对视图内的字符计算哈希。

  • 示例对比:

    1
    2
    3
    4
    5
    // 传统方法(需构造临时 string)
    std::hash<std::string>{}(std::string(data, length)); // 涉及内存分配与复制

    // 使用 string_view(零拷贝)
    std::hash<std::string_view>{}(std::string_view(data, length)); // 直接计算
  1. 兼容性
  • std::string 哈希兼容:相同内容的stringstring_view生成相同哈希值。

    1
    2
    3
    std::string str = "hello";
    std::string_view view = str;
    assert(std::hash<std::string>{}(str) == std::hash<std::string_view>{}(view));
  1. 减少内存分配
  • 适用于临时字符串:如解析文本时的子串,避免频繁创建std::string

    1
    2
    3
    4
    5
    void processSubstring(const char* data, size_t len) {
    std::string_view view(data, len);
    size_t hash = std::hash<std::string_view>{}(view); // 无内存分配
    // ...
    }

底层实现原理

std::hash<std::string_view> 的实现通常基于以下原则:

  1. 遍历字符计算哈希

    • 对视图内的每个字符进行迭代,使用如 FNV-1a、MurmurHash 等算法生成哈希值。

    • 示例伪代码:

      1
      2
      3
      4
      5
      6
      7
      size_t operator()(std::string_view s) const {
      size_t hash = 0;
      for (char c : s) {
      hash = (hash * 101) ^ static_cast<unsigned char>(c); // 简化版
      }
      return hash;
      }
  2. 优化处理长字符串

    • 某些实现可能跳过部分字符(如每隔几个字符处理一次),在速度和分布性之间权衡。

应用场景

  1. 解析协议 / 文件
  • 示例:解析 HTTP 请求头时,快速哈希路径部分:

    1
    2
    3
    4
    5
    6
    7
    // 请求行: "GET /api/users HTTP/1.1"
    const char* data = ...; // 请求数据
    size_t path_start = 4;
    size_t path_length = 10; // 假设已解析出路径长度

    std::string_view path(data + path_start, path_length);
    size_t hash = std::hash<std::string_view>{}(path); // 直接哈希,无需构造 string
  1. 缓存键优化
  • 场景:使用字符串片段作为缓存键,避免复制:

    1
    2
    3
    4
    5
    std::unordered_map<std::string_view, CacheValue, std::hash<std::string_view>> cache;

    void addToCache(std::string_view key, CacheValue value) {
    cache[key] = value; // 注意:需确保 key 的生命周期覆盖缓存使用期
    }
  1. 字符串去重
  • 需求:快速判断重复子串(如编译器中的标识符):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    std::unordered_set<std::string_view, std::hash<std::string_view>> seen;

    void processToken(std::string_view token) {
    if (seen.count(token)) {
    // 重复处理
    } else {
    seen.insert(token);
    }
    }

4.4.6 reinterpret_cast

reinterpret_cast<const char *>(encoded.data()) 是一个 C++ 类型转换表达式,其作用是将 encoded.data() 返回的指针强制转换为 const char* 类型。

reinterpret_cast<const char *>(...)

  • 强制类型转换reinterpret_cast 是 C++ 中最底层的类型转换操作符,用于将一种指针类型转换为另一种不相关的指针类型(如 uint8_t*const char*)。
  • 内存视角:该转换不改变指针的值(即内存地址),仅改变编译器对该内存的解释方式:
    • 原内存被视为 uint8_t 数组(每个元素为 1 字节无符号整数)。
    • 转换后被视为 const char 数组(字符串或字符流)。

4.4.7 **vector::reserve **

  • std::vector::reserve(size_t n) 会为向量预分配至少 n 个元素的内存空间,但不改变向量的大小(size() 仍为 0)。
  • 目的是减少后续插入操作时的内存重新分配次数,提高性能。

4.4.8 vector::assign

  • std::vector::assign(first, last)清空向量并复制 [first, last) 区间内的元素到向量中。

内存分配与复制过程

  • 由于之前已调用 reserve(offset_section_start)block->data 的容量(capacity())已足够存储 offset_section_start 个元素,因此 assign 不会触发额外的内存分配。
  • 数据从 encoded 复制到 block->data,覆盖 block->data 原有内容(若有)。

4.4.9 string构造重载

1
2
3
4
std::string::string(
const char* s, // 指向字符数组的指针
size_t n // 要复制的字符数
);

4.4.10 string::compare

1
int compare(const std::string& str) const noexcept;
  • 功能:将当前字符串与 str 进行逐字符比较。
  • 返回值:
    • 负数:当前字符串 < str
    • 0:当前字符串 == str
    • 正数:当前字符串 > str
1
2
3
4
key.compare(target);
// key<target return <0
//key==target return =0
//key>target return >0

4.4.11 share_ptr

  1. 推荐方式:std::make_shared

    • 优点:一次分配同时创建对象和控制块,避免内存碎片

    • 语法:std::make_shared<T>(args...)

    • 示例:

      1
      auto ptr = std::make_shared<std::string>("hello");
  2. 原始指针构造(谨慎使用)

    • 直接从裸指针构造,但需注意所有权转移:

      1
      2
      3
      int* raw = new int(10);
      std::shared_ptr<int> sp1(raw); // sp1获取raw所有权
      std::shared_ptr<int> sp2(raw); // 危险!重复释放
    • 禁止将同一裸指针传入多个shared_ptr,会导致双重释放。

  3. 从其他智能指针转换

    • unique_ptr转移所有权:

      1
      2
      std::unique_ptr<int> up(new int(20));
      std::shared_ptr<int> sp(std::move(up)); // up失去所有权

4.4.12 mutable

在 C++ 中,mutable 是一个关键字,用于修饰类的成员变量。当一个成员变量被声明为 mutable 时,它表示这个变量即使在 const 成员函数中也可以被修改。这一特性在实现缓存、懒加载或线程同步等场景中非常有用。

mutable 的核心作用

  1. 突破 const 限制

const 成员函数中,默认所有非 mutable 的成员变量都不能被修改。但 mutable 变量是个例外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Example {
private:
mutable int counter; // 可变计数器
std::string data; // 普通成员变量

public:
void doSomething() const {
// 错误:不能修改非 mutable 变量
// data = "new value"; // 编译错误

// 允许:可以修改 mutable 变量
counter++; // 合法
}
};
  1. 保持逻辑常量性

mutable 允许在不影响对象逻辑状态的前提下修改某些内部状态。例如:

  • 缓存计算结果(如你提到的 cached_value
  • 记录函数调用次数
  • 实现线程安全的锁机制

在缓存场景中的应用

  1. 缓存值的更新

在你的代码中,cached_value 用于缓存当前迭代器的值。即使在 const 成员函数中(如 operator*()),也需要更新这个缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
class BlockIterator {
public:
value_type operator*() const {
if (!cached_value.has_value()) {
// 首次访问时计算并缓存值
cached_value = computeValue(); // 即使在 const 函数中也能修改
}
return *cached_value;
}

private:
mutable std::optional<value_type> cached_value; // 缓存值,可变
};

mutable std::optional<value_type> cached_value 的作用是:
允许在 const 成员函数中更新缓存值,同时保持迭代器的逻辑常量性

这种设计使得迭代器的 const 接口(如 operator*())可以安全地使用缓存优化,而不违反 const 的语义。在现代 C++ 中,这是实现高效且安全的缓存机制的常见做法。

4.4.13 匿名函数

1
2
3
auto func = [&preffix](const std::string &key) {
return -key.compare(0, preffix.size(), preffix);
};

这是一种 C++ 中的 Lambda 表达式(匿名函数),很多人第一次看会觉得“怪”,但其实它是非常实用的临时函数对象写法。

C++ 的 lambda 表达式完整结构如下:

1
[捕获列表](参数列表) -> 返回类型 { 函数体 }

你这个语句:

1
2
3
auto func = [&preffix](const std::string &key) {
return -key.compare(0, preffix.size(), preffix);
};

对照结构就是:

部分 含义
[&preffix] 捕获外部变量 preffix(以引用的方式)
(const std::string &key) 这是 lambda 的参数,接受一个 key
{ ... } 是函数体,返回一个 int
auto func = ... 把 lambda 函数赋值给变量 func,以后就可以当成函数用了

这段 lambda 定义了一个字符串前缀比较函数,它的逻辑是:

1
return -key.compare(0, preffix.size(), preffix);

它比较:key 的前 preffix.size() 个字符是否等于 preffix。并且它把 std::string::compare() 的结果取反,让它的返回值满足 get_monotony_predicate_iters 所要求的“方向语义”。

4.4.14 lambda 表达式

普通函数的写法:

1
2
3
bool is_even(int x) {
return x % 2 == 0;
}

用 lambda 表达式写:

1
2
3
auto is_even = [](int x) {
return x % 2 == 0;
};

和普通函数作用一模一样,只不过是匿名函数,直接用变量保存。

捕获列表

它是让 lambda 可以访问外部变量 的机制!

不捕获变量(只能用参数):

1
2
3
auto f = [](int x) {
return x + 1;
};

捕获变量(让函数体能访问外部变量):

1
2
3
4
5
6
7
int base = 10;

auto f = [base](int x) { // 捕获base的值
return x + base;
};

f(5); // 结果 15

[] 中的 base 表示:把外部的 base 变量值复制进来(值捕获)

捕获方式详解

写法 意义
[base] 只捕获 base(值捕获,复制一份)
[&base] 捕获 base 的引用(改外部也会生效)
[=] 把用到的所有变量都值捕获(拷贝)
[&] 把用到的所有变量都引用捕获
1
2
3
4
5
int a = 3, b = 4;

auto f1 = [=]() { return a + b; }; // 捕获 a 和 b 的值
auto f2 = [&]() { return a + b; }; // 捕获 a 和 b 的引用
auto f3 = [a, &b]() { return a + b; }; // 捕获 a 的值, b 的引用

4.4.15 string::assign

std::string::assign 是 C++ 标准库中用于修改字符串内容的成员函数,它提供了灵活的方式来替换当前字符串的内容。与赋值运算符 operator= 相比,assign 支持更多的参数形式(如子串、迭代器范围等),且允许链式调用。

一、函数原型与重载形式

  1. 用另一个字符串赋值
1
2
string& assign(const string& str);                // 拷贝赋值
string& assign(string&& str) noexcept; // 移动赋值(C++11 起)
  1. 用 C 风格字符串赋值
1
2
string& assign(const char* s);                    // 以 '\0' 结尾的字符串
string& assign(const char* s, size_t n); // 取 s 的前 n 个字符
  1. 用重复字符赋值
1
string& assign(size_t n, char c);                 // 重复 n 次字符 c
  1. 用迭代器范围赋值
1
2
template <class InputIt>
string& assign(InputIt first, InputIt last); // 赋值 [first, last) 范围的字符
  1. 用初始化列表赋值
1
string& assign(initializer_list<char> il);        // C++11 起,用花括号初始化列表

二、典型使用场景

  1. 替换为另一个字符串的子串
1
2
std::string str = "hello";
str.assign("world", 3); // str 变为 "wor"
  1. 用重复字符填充字符串
1
2
std::string padding;
padding.assign(10, '-'); // padding 变为 "----------"
  1. 从迭代器范围赋值
1
2
3
std::vector<char> chars = {'a', 'b', 'c', 'd'};
std::string str;
str.assign(chars.begin(), chars.end()); // str 变为 "abcd"
  1. 链式调用
1
2
std::string str;
str.assign("hello").append(" world"); // str 变为 "hello world"

三、与赋值运算符 operator= 的对比

操作符 / 函数 特点 示例
str = other 简洁,仅支持完整赋值 str = "abc";
str.assign(...) 灵活,支持子串、迭代器等形式 str.assign("abc", 2);

四、性能考虑

  1. 内存优化:若新字符串长度小于当前容量,assign 通常不会重新分配内存。
  2. 移动语义:使用右值引用参数时(如 assign(std::move(other))),避免深拷贝,仅转移资源所有权。

4.4.16 FileObj::create_and_write

1
FileObj file=FileObj::create_and_write(path, file_content); 

一、代码逐段解析

  1. FileObj::create_and_write(...)
  • 静态工厂方法create_and_writeFileObj 类的==静态成员函数==,无需创建对象即可调用。
  • 功能推测:该方法负责创建文件、写入内容,并返回一个 FileObj 对象。
  1. 返回值类型
  • FileObj::create_and_write 的返回类型是 FileObj,通过值返回一个新对象。
  1. 对象初始化
  • 现代 C++ 优化:编译器通常会通过 RVO(返回值优化)NRVO(具名返回值优化) 避免实际的拷贝或移动操作。

二、静态工厂方法的优势

  1. 封装构造逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FileObj {
private:
// 私有构造函数,禁止外部直接创建
FileObj(const std::string& path, bool is_open) { ... }

public:
// 静态工厂方法
static FileObj create_and_write(const std::string& path,
const std::string& content) {
// 1. 创建文件
// 2. 写入内容
// 3. 返回 FileObj 对象
return FileObj(path, true);
}
};
  1. 控制对象创建
  • 可返回子类对象(如 EncryptedFileObj)。
  • 实现单例模式或对象池。
  1. 提供语义化接口
  • 方法名(如 create_and_write)比构造函数更直观。

4.4.17 FileObj::read_to_slice

FileObj::read_to_slice 是一个典型的静态成员函数,用于从文件读取数据并填充到指定的内存区域(slice)。这种设计在文件处理、数据库系统和网络编程中非常常见。下面从功能、实现和使用场景三个方面详细解析:

  1. 函数定位
  • 静态工厂方法:通过类名直接调用(如 FileObj::read_to_slice(...))。
  • 数据传输:将文件内容读取到预先分配的内存缓冲区(slice)。
  1. 典型参数与返回值
1
2
3
4
5
6
static bool read_to_slice(
const std::string& path, // 文件路径
uint8_t* dest_buffer, // 目标缓冲区指针
size_t buffer_size, // 缓冲区大小
size_t* out_bytes_read = nullptr // 可选:实际读取的字节数
);

4.4.18 二分查找范围

一般情况下,二分查找的范围都是闭区间

标准写法(左右闭区间):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int binary_search(const vector<int>& arr, int target) {
int left = 0;
int right = arr.size() - 1; // ✅ 注意这里是 size()-1

while (left <= right) {
int mid = left + (right - left) / 2;

if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1; // ✅ 缩小范围
} else {
right = mid - 1; // ✅ 缩小范围
}
}
return -1;
}

这是“左右闭区间”的典型写法,合法范围为 [left, right]right = size() - 1while (left <= right)

左闭右开区间 [left, right) 的二分变种,这是在很多 STL 风格代码里也常见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t left = 0;
size_t right = meta_entries.size(); // ✅ 注意 right 是 size()

while (left < right) {
size_t mid = (left + right) / 2;

if (...) {
right = mid; // 排除 mid 及右侧
} else if (...) {
left = mid + 1; // 排除 mid 及左侧
} else {
return mid; // 找到
}
}

这个写法在某些需要上界(upper_bound)、下界(lower_bound)功能的时候更方便,不用担心 right = size() 越界的问题。


总结:两种写法对比

区间类型 条件 初始化 right 优点
左闭右闭 left <= right right = size - 1 更直观、教学常见
左闭右开 left < right right = size STL 风格、可避免越界

每一轮,区间都会被一分为二,保留一半,继续查找。

模式 区间表示方式 循环条件 每轮查找时区间内容
闭区间 [left, right] left <= right 包含 leftright
开区间 [left, right) left < right 包含 left,不包含 right

[left, right) 表示:我们要找的答案一定在 [left, right-1] 中。为什么 left < right 作为循环条件?因为当 left == right 时,这个区间变成了空区间(没有任何数字)!此时:区间 [left, right) 变成了 [x, x),里面没有数。所以不需要继续查找了,该退出了。

五、LSMEngine

5.1 engine

5.2 compact&&Iterator

level0阈值达到后就要合并:

  1. 首先, Level 0SST之间存在key的重叠, 需要进行去重, 这里我们可以复用已有的HeapIterator, 见Lab 2.2 迭代器
  2. 其次就是我们将要实现的ConcactIterator, 这个迭代器会串联Level 1的所有SST形成层间迭代器
  3. 最后就是之后小节实现的TwoMergeIterator, 这个迭代器会串联HeapIteratorConcactIterator, 按照优先级对迭代器进行输出, 遍历TwoMergeIterator即可构建新的Level 1SST

5.2.1 Concact Iterator

这个迭代器的作用是会串联Level 1的所有SST形成层间迭代器。

5.2.2TwoMergeIterator

整合两个优先级不同的迭代器(HeapIterator和ConcactIterator), 生成一个新的迭代器, 按照优先级对迭代器进行输出

遍历TwoMergeIterator即可构建新的Level 1的SST

5.2.3 Size-Tiered Compaction

Size-Tiered Compaction 按照「大小」和「层级」进行文件合并。

  • 每层允许多个 SSTable(大致等大小)并存。
  • 如果一层的 SSTable 数量超过阈值,就触发合并。
  • 合并后的文件推送到下一层

流程举例,初始状态(假设每层最多允许 3 个 SSTable):

1
2
3
Level 0: [SST1] [SST2] [SST3]
Level 1: [SST4] [SST5]
Level 2: [SST6]
  • Level 0 现在有了 3 个 SSTable,达到了阈值。
  • 所以触发一次合并,把 [SST1] [SST2] [SST3] 合并成一个新文件,比如 SST7
  • 合并后,这个文件就被推到 Level 1。

合并后状态:

1
2
3
Level 0: []
Level 1: [SST4] [SST5][Merged(SST1, SST2, SST3)] // 新的 SST7
Level 2: [SST6]

如果 Level 1 的文件数量也超过了阈值,那么这层也会触发合并,递归向下推送。

优点 说明
写入速度快 写完数据就 flush 成新 SSTable,后续再慢慢合并
实现简单 合并逻辑比较直观:到数量阈值就合并
缺点 说明
读放大严重 一个 key 可能出现在多个层,甚至多个 SSTable,读时需要查很多文件
延迟不可控 合并时需要读取多个大文件再写入,对系统资源要求高

读放大(Read Amplification):一次查询实际涉及的数据读取量远超你真正要的数据。

你要查 key=”user123”,但是:

  • 它可能在 Level 0 的某个表
  • 也可能在 Level 1 的某个表
  • 也可能在 Level 2 的表……

你就要在多个 SSTable 中找,很浪费资源。

Size-Tiered 适用场景:

  • 写入压力很大、但查询频率低的系统。
  • 例如日志数据库、大数据离线处理(HBase 的默认策略就是 Size-Tiered)。

5.2.4 Leveling Compaction

Leveling 是另一种在 LSM 树中使用的压缩策略,常用于 读性能优先 的数据库(比如 RocksDB)。

它的核心思想是:

  • 每一层(Level)中 SSTable 的 key 范围是互不重叠的
  • 一旦 Level L 的某个 SSTable 与 Level L+1 的范围冲突,就会触发合并,将其写入到 L+1。
  • 每一层的空间大小按指数级增长,比如:
Level 容量(举例)
L0 20MB
L1 100MB
L2 500MB
L3 2GB
维度 Size-Tiered Compaction (STC) Leveling Compaction
合并触发 文件数超过阈值就合并 空间或范围冲突时合并
同层 key 是否重叠 ✅ 允许重叠 ❌ 不允许重叠
合并粒度 多文件合成一个大文件 一个文件与多个冲突文件合并
写放大 较小(写入快) 较高(频繁写入下层)
读放大 较大(需查多个文件) 较小(只查一个 SSTable)
适用场景 写多读少,如日志 读多读快,如数据库 OLTP

Leveling 的流程示意

初始状态:

1
2
3
Level 0: [SST1] [SST2]
Level 1: [SST3: key=a~f]
[SST4: key=g~z]

如果 SST2 的 key 范围是 e~m,则与 Level 1 的 SST3SST4 都有交集。

压缩时:

  • 会将 SST2 与冲突的 SST3SST4 合并,生成新的 SST5: key=a~z
  • 替换掉 SST3/SST4

合并后状态:

1
2
Level 0: [SST1]
Level 1: [SST5: key=a~z]
优点 说明
读性能好 同一层不重叠,查 key 时只需在每层查一个文件
空间效率好 不会重复存多个版本,删除/覆盖旧值更及时
快速过期旧数据 很容易定位和清理 TTL 数据
缺点 说明
写放大高 数据会不断从 L0 → L1 → L2 递推,写入下层
Compaction 更频繁 L0 和 L1 之间合并密集,需要 CPU 和 IO 资源

Leveling vs Size-Tiered

1
2
3
4
5
6
7
8
9
Leveling:
L0: [a~m] [n~z] // key 范围可能重叠

L1: [a~z] // 合并后 L1 不允许重叠

Size-Tiered:
L0: [a~m], [c~n], [b~r] // 同层可重叠

L1: [Merged(a~r)] // 合并所有 → 写入下一层

一个混合策略:Universal Compaction(混合)

很多现代 LSM 引擎(如 RocksDB)支持一种 混合策略

  • 写入初期采用 Size-Tiered,提高吞吐;
  • 数据量变大后切换为 Leveling,提高查询性能。

5.2.5 缓存池

缓存池只是通过智能指针把block对象留在内存里,不释放block对象

5.2.6 布隆过滤器

布隆过滤器测试代码修改位置。

LSMEngine::flush()

1
2
3
// 3. 准备 SSTBuilder
SSTBuilder builder(TomlConfig::getInstance().getLsmBlockSize(),
true); // 4KB block size

LSMEngine::full_common_compact

1
2
auto new_sst_builder =
SSTBuilder(TomlConfig::getInstance().getLsmBlockSize(), false);

补充知识

5.1.1 map

1
std::map<size_t, std::deque<size_t>> level_sst_ids;

这个变量表示每层的SST

std::map<KeyType, ValueType> 是一个将 Key 映射到 Value 的容器,Key 是唯一的,并自动排序。

初始化:

1
2
level_sst_ids[0] = {1, 2};
level_sst_ids[1] = {3, 4, 5};

map::find

findmap 提供的一个函数,用于查找某个 key 是否存在

  • 如果找到,返回指向该元素的迭代器(即 map<Key, Value>::iterator
  • 如果没找到,返回 map.end()(末尾迭代器)

插入方式:

1
2
age["Tom"] = 20;  // 推荐,简洁且自动插入
age.insert({"Jerry", 18}); // 显式插入(pair)

访问元素

1
cout << age["Tom"];   // 若 Tom 不存在,会创建默认值 0

要避免默认插入,可以先用 find

1
2
if (age.find("Lucy") != age.end())
cout << age["Lucy"];

std::map

  • 使用红黑树(一种自平衡的二叉查找树)
  • key 会自动排序
  • 所以支持有序遍历、范围查找
1
2
3
4
5
6
7
cpp复制编辑std::map<int, std::string> m;
m[2] = "b";
m[1] = "a";
m[3] = "c";

// 输出顺序:1, 2, 3(自动排序)
for (auto &[k, v] : m) std::cout << k << " ";

std::unordered_map

  • 使用哈希表
  • key 不排序,插入顺序不可预测
  • 查找效率更快(理想情况下 O(1))
1
2
3
4
5
6
7
cpp复制编辑std::unordered_map<int, std::string> m;
m[2] = "b";
m[1] = "a";
m[3] = "c";

// 输出顺序:无序(哈希桶顺序)
for (auto &[k, v] : m) std::cout << k << " ";

5.1.2 is_regular_file

is_regular_file() 是 C++17 标准库 <filesystem> 中的一个成员函数,用于判断某个目录项(directory_entry 对象)是否是一个普通文件(regular file)

在操作系统中,regular file(常规文件)指的是最普通的文件类型:

  • 不是目录(directory)
  • 不是符号链接(symlink)
  • 不是块设备、字符设备、FIFO、socket 等特殊文件
  • 它包含的是普通的用户数据(文本文件、二进制文件、图像文件等)

🌟 举例

假设你使用 std::filesystem 来遍历一个文件夹:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
for (const auto& entry : fs::directory_iterator("some_directory")) {
if (entry.is_regular_file()) {
std::cout << entry.path() << " 是一个普通文件。\n";
}
}
return 0;
}

这段代码只会输出“普通文件”的路径,跳过目录、符号链接等其他类型。


📌 相关函数还有:

  • entry.is_directory():是否是目录
  • entry.is_symlink():是否是符号链接
  • entry.is_block_file():是否是块设备
  • entry.is_character_file():是否是字符设备
  • entry.is_fifo():是否是 FIFO(管道)
  • entry.is_socket():是否是 socket

如果你在用的是 Linux、macOS 或支持 POSIX 的系统,这些类型在 ls -l 命令中也能看到,例如 -rw-r--r-- 前缀的就是 regular file。

5.1.3 std::stoull()

把字符串 str 转换成一个无符号整数(unsigned long long

5.1.4 std::unordered_map

std::unordered_map 是 C++ 标准库中提供的一种 哈希表(Hash Table)容器,属于 <unordered_map> 头文件。

它是一个 键值对(key → value)映射表,并且相较于 std::map

  • 访问速度更快(平均 O(1))
  • 内部元素无序(不是按 key 排序的)

std::unordered_map<Key, Value> 是一个 基于哈希表实现的字典型容器,允许你通过 key 快速存取、插入、删除 value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <unordered_map>

int main() {
std::unordered_map<std::string, int> age;
age["Alice"] = 25;
age["Bob"] = 30;
std::cout << "Alice's age: " << age["Alice"] << std::endl; // 25
// 查找
if (age.find("Bob") != age.end()) {
std::cout << "Bob is in the map.\n";
}
// 遍历
for (auto& [name, a] : age) {
std::cout << name << ": " << a << std::endl;
}
return 0;
}

自定义哈希:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 自定义哈希函数
struct pair_hash {
template <class T1, class T2>
std::size_t operator()(const std::pair<T1, T2> &p) const {
auto hash1 = std::hash<T1>{}(p.first);
auto hash2 = std::hash<T2>{}(p.second);
return hash1 ^ hash2;
}
};

// 自定义相等比较函数
struct pair_equal {
template <class T1, class T2>
bool operator()(const std::pair<T1, T2> &lhs,
const std::pair<T1, T2> &rhs) const {
return lhs.first == rhs.first && lhs.second == rhs.second;
}
};

std::unordered_map<std::pair<int, int>, std::list<CacheItem>::iterator,
pair_hash, pair_equal>

为什么要写 pair_hashpair_equal

默认情况下,std::unordered_map 只能对 内置类型(如 int, string 等)做哈希。std::pair<int, int> 是复合类型,标准库默认没有哈希支持。

所以需要自己定义:

  • 哈希函数(告诉哈希表怎么计算 key 的哈希值)
  • 相等比较器(告诉哈希表怎么判断两个 key 相等)
参数位置 类型 含义
1 std::pair<int, int> 键类型,复合 key
2 std::list<CacheItem>::iterator 值类型,通常是某个缓存结构中 list 的迭代器
3 pair_hash 自定义哈希函数(告诉哈希表如何哈希 pair)
4 pair_equal 自定义相等函数(告诉哈希表如何比较 pair)

哈希函数 pair_hash 怎么工作?

1
2
3
4
5
6
7
8
struct pair_hash {
template <class T1, class T2>
std::size_t operator()(const std::pair<T1, T2> &p) const {
auto hash1 = std::hash<T1>{}(p.first); // 哈希第一个元素
auto hash2 = std::hash<T2>{}(p.second); // 哈希第二个元素
return hash1 ^ hash2; // 异或混合
}
};
  • 使用标准库已有的 std::hash<T>pair 的两个成员分别计算哈希
  • ^(按位异或)合并成一个整体哈希值

相等函数 pair_equal 怎么工作?

1
2
3
4
5
6
7
struct pair_equal {
template <class T1, class T2>
bool operator()(const std::pair<T1, T2> &lhs,
const std::pair<T1, T2> &rhs) const {
return lhs.first == rhs.first && lhs.second == rhs.second;
}
};
  • 判断两个 pair 是否“相等”:两个元素分别都相等才算相等

为什么你需要这样用?(应用场景)

如果你想把一个二元 key(如 LRU-K 中的 (key, cache_id))作为哈希表的索引:

1
std::unordered_map<std::pair<int, int>, ListIterator, pair_hash, pair_equal>

就必须让哈希表“知道”怎么处理 pair<int, int>

标准库不知道 pair 应该怎么哈希,但你可以教它。

当你想把 std::pairstd::tuple 或自定义类当作 unordered_map 的 key,用法是:

1
std::unordered_map<KeyType, ValueType, HashFunc, EqualFunc>

必须给它提供:

  • HashFunc: 计算 key 的哈希值(即在哪个桶)
  • EqualFunc: 判断两个 key 是否“相同”(即是否放进同一桶)

5.1.5 std::list::splice()

splice()std::list 的特有成员函数,用于**“无拷贝地”把元素从一个 list 移动到另一个 list**,或在同一个 list 内部移动元素位置

它的原型有几种重载,项目里用的是这一个:

1
void splice(iterator pos, list& other, iterator it);
1
cache_list_less_k.splice(cache_list_less_k.begin(), cache_list_less_k, it);
参数位置 含义
第1个参数 cache_list_less_k.begin() 要插入的位置:把 it 插入到链表头部
第2个参数 cache_list_less_k 要提取元素的源链表(这里是自己)
第3个参数 it 要移动的元素的位置(一个迭代器)

所以这句话做的事是:

cache_list_less_k 自己内部,把元素 it 移动到头部(begin())的位置。

  • 不拷贝,不新建对象
  • 只是改变链表节点的前后指针关系(O(1)
  • 是 LRU 更新位置的标准写法

假设原来链表是这样:

1
2
3
cache_list_less_k: [A, B, C, D]

it

执行:

1
cache_list_less_k.splice(cache_list_less_k.begin(), cache_list_less_k, it);

结果:

1
cache_list_less_k: [C, A, B, D]
  • C 原地移动到了头部(begin()
  • it 指向的那个元素被挪到了第一个位置
  • 所有指针仍然有效

为什么不用 erase() + insert()

因为:

  • erase() + insert() 会导致 对象销毁再重建,效率低,还会破坏 shared_ptr 引用
  • splice()真正的原地移动,对 LRU 这种场景非常高效且安全

六、事务

6.1 事务管理器

6.1.1 atomic

atomic 是 C++11 引入的 原子操作类型(std::atomic,用于在多线程环境下保证数据操作的原子性,避免竞态条件(race condition)。

普通遍历实现事务 ID 分配的问题:

1
2
3
4
5
uint64_t next_txn_id = 0;

uint64_t getNextTxnId() {
return next_txn_id++;
}

这在单线程下没问题,但多线程时可能发生如下问题:

  • 线程 A 读到的是 100
  • 线程 B 也同时读到 100
  • 两人都写入 101

事务 ID 重复,系统崩了!

使用 std::atomic 保证线程安全:

1
2
3
4
5
6
7
#include <atomic>

std::atomic<uint64_t> next_txn_id{0};

uint64_t getNextTxnId() {
return next_txn_id.fetch_add(1, std::memory_order_relaxed);
}

这段代码的意思是:

  • 多个线程同时调用 getNextTxnId()
  • fetch_add(1) 是一个原子操作,只会让其中一个线程得到 100,下一个是 101,再下一个 102……
  • 这样就保证了每个事务都有唯一 ID

6.2 Wal

6.2.1 std::thread

std::thread 是 C++11 引入的标准库类,用于创建和管理线程。它属于 <thread> 头文件,是现代 C++ 中多线程编程的核心组件之一。

std::thread 是一个 轻量级封装的线程对象类,可以让你并行执行一个函数、lambda 或成员函数,并对该线程进行控制(启动、等待、分离等)。

创建线程

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <thread>

void hello() {
std::cout << "Hello from thread!\n";
}

int main() {
std::thread t(hello); // 创建并启动一个线程,执行 hello()
t.join(); // 等待线程执行完毕
return 0;
}

使用 lambda 创建线程

1
2
3
4
std::thread t([] {
std::cout << "Hello from lambda thread!\n";
});
t.join();

常见成员函数

函数 说明
join() 阻塞当前线程,直到该线程执行完
detach() 分离线程,使其在后台运行(不能再 join)
joinable() 判断线程是否可以 join
get_id() 获取线程 ID
native_handle() 获取底层的线程句柄(如 pthread handle)

线程函数类型支持

  1. 普通函数(如 void func()
  2. Lambda 表达式
  3. 函数对象(仿函数)
  4. 类的成员函数(需要传对象指针)
1
2
3
4
5
6
7
8
9
class Worker {
public:
void do_work(int x) {
std::cout << "Work: " << x << "\n";
}
};

Worker w;
std::thread t(&Worker::do_work, &w, 42); // 成员函数

后台线程(如 WAL cleaner)

1
2
3
4
5
6
7
8
9
10
void cleaner() {
while (!stop_flag) {
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "Cleaning logs...\n";
}
}

std::thread cleaner_thread(cleaner); // 启动后台线程
...
cleaner_thread.join(); // 等待它退出

常见注意点

项目 说明
必须 join 或 detach 否则程序结束时会 terminate
不可复制 std::thread 只能移动 std::move(thread)
加锁同步 多线程间访问共享变量需要互斥锁(如 std::mutex

七、兼容Redis

7.1 Resp 协议简介

Resp(REdis Serialization Protocol,Redis 序列化协议)是 Redis 服务器与客户端之间进行数据交换的专用应用层协议,由 Redis 团队设计,核心目标是在 “简单易实现” 与 “高效传输” 之间取得平衡,确保客户端(如 Python 的redis-py、Java 的Jedis)能轻松与 Redis 服务器通信,同时最小化数据传输开销。

7.1.1Resp 协议的核心设计目标

Resp 的设计围绕以下 3 个核心需求展开,这也是其被广泛应用于 Redis 生态的关键原因:

  1. 简单易实现:协议格式直观,仅通过少量特殊字符(如$*+等)标识数据类型,开发者无需复杂解析逻辑即可编写客户端;
  2. 高效紧凑:数据格式无冗余字段,仅包含 “类型标识 + 数据长度 + 数据内容”,减少网络传输的字节数;
  3. 人类可读:协议的文本化特性(非二进制)使其支持通过telnet等工具直接调试,便于问题排查(例如通过telnet 127.0.0.1 6379手动发送 Resp 命令)。

7.1.2 Resp 协议的核心数据类型(Resp 2.0)

Resp 协议通过首字符标识数据类型,不同类型对应不同的格式规则。目前 Redis 主要使用 Resp 2.0(Resp 3.0 为扩展版本,支持更多复杂类型,尚未完全普及),其核心数据类型如下表所示:

数据类型 首字符 格式说明 示例(客户端→服务器 / 服务器→客户端)
简单字符串 + + + 字符串内容 + \r\n(结尾固定为回车换行,下同) 服务器返回成功:+OK\r\n
错误信息 - - + 错误类型 + 错误描述 + \r\n(客户端需捕获并处理) 键不存在:-ERR no such key\r\n
整数 : : + 整数数值 + \r\n(用于返回计数、状态码等,如LLEN的结果) LLEN list1的结果::5\r\n(表示列表长度为 5)
批量字符串 $ $ + 字符串长度(字节数) + \r\n + 字符串内容 + \r\n(支持空字符串) 客户端发送键"name"$4\r\nname\r\n;空字符串:$0\r\n\r\n
数组 * * + 数组元素个数 + \r\n + 每个元素(任意 Resp 类型) 客户端发送SET name Alice*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nAlice\r\n
空值 $ 特殊的批量字符串,格式为$-1\r\n(表示 “无数据”,如GET 不存在的键 服务器返回空值:$-1\r\n

7.1.3 Resp 协议的通信流程(以 “客户端发命令 + 服务器回结果” 为例)

Resp 的通信逻辑完全基于 “请求 - 响应” 模型,流程如下:

  1. 客户端构造请求(数组类型)
    Redis 命令(如SETGETHGETALL)本质是 “命令名 + 参数” 的集合,客户端需将其封装为 Resp 数组:
    • 首字符*后接 “元素个数”(命令名 + 参数的总数量);
    • 每个元素(命令名、参数)用 “批量字符串” 表示(需指定长度)。
      示例:发送GET user:100(命令名GET,参数user:100),请求格式为:
      *2\r\n$3\r\nGET\r\n$7\r\nuser:100\r\n
  2. 服务器解析请求并处理
    Redis 服务器接收字节流后,按 Resp 规则解析:
    • 先读取首字符*,确定为数组类型,再读取2(元素个数);
    • 依次解析 2 个批量字符串:第一个是GET(长度 3),第二个是user:100(长度 7);
    • 执行GET user:100命令,获取结果(如用户信息"Alice")。
  3. 服务器构造响应(对应 Resp 类型)
    服务器根据命令执行结果,选择对应的 Resp 类型返回:
    • 若结果为字符串(如GET成功):返回批量字符串,格式为$5\r\nAlice\r\n
    • 若结果为整数(如INCR count):返回整数,格式为 :10\r\n
    • 若命令无返回值(如SET成功):返回简单字符串,格式为+OK\r\n
    • 若命令执行失败(如键不存在):返回错误信息,格式为-ERR no such key\r\n
  4. 客户端解析响应并展示
    客户端读取服务器返回的字节流,按 Resp 规则解析为本地数据类型(如 Python 的strint,Java 的StringLong),最终呈现给用户。

7.1.4 Resp 3.0:Resp 2.0 的扩展(可选了解)

Resp 2.0 仅支持上述 6 种基础类型,无法满足复杂场景(如区分 “字符串” 和 “数字”、“布尔值”)。Resp 3.0 在兼容 2.0 的基础上,新增了更多类型,例如:

  • 布尔值:首字符##t\r\n表示true#f\r\n表示false
  • 浮点数:首字符,,如,3.14\r\n
  • 有序映射:首字符%,用于返回键值对(如HGETALL的结果可直接用映射表示,无需解析数组);
  • 大整数:首字符(,用于超出 64 位的整数。

目前 Redis 6.0 + 已部分支持 Resp 3.0,但多数客户端仍以兼容 Resp 2.0 为主,如需使用高级特性需确认客户端支持度(如redis-py 4.0 + 支持 Resp 3.0)。

7.1.5 Resp 协议的优势与局限性

优势

  1. 低门槛:文本化格式 + 简单规则,新手可快速编写基础客户端(如用 Python 的socket直接发送 Resp 格式的字节流);
  2. 高性能:无冗余字段,数据紧凑,减少网络 IO 耗时(Redis 的高性能部分依赖于 Resp 的高效性);
  3. 跨语言兼容:仅依赖 “字节流解析”,无关编程语言(C、Java、Python、Go 等均有成熟客户端);
  4. 可调试性:支持telnetncnc 127.0.0.1 6379)手动发送命令,便于定位通信问题。

局限性

  1. 仅适用于 Redis:Resp 是 Redis 专用协议,无法用于其他数据库(如 MySQL、MongoDB);
  2. 文本格式的局限:相比二进制协议(如 Protocol Buffers),文本格式在传输超大二进制数据(如图片)时,效率略低(需额外传输长度字段的字符);
  3. Resp 2.0 类型不足:需通过 “约定” 区分数据语义(如用字符串存储 JSON,客户端需自行解析),Resp 3.0 虽缓解此问题,但普及度有限。

7.2 字符串类型

7.3 哈希类型

对于hash类型 重点实现4个函数hsethgethdelhkeys

  • 对于hset 先检查是否过期,如果key 过期 就释放读锁 加写锁 如何key没过期,就直接操作(field_key,value )插入lsm,同时对key的value部分进行字段更新,如果存在就不做操作,不存在。就插入。

  • 对于hget 先检查是否过期,如果key 过期 就返回 $-1\r\n 否则查询

  • 对于hdel(删除一个哈希类型的key的某个字段) 先检查key是否过期 ,如果过期返回 :0\r\n ;否则删除 key_field 然后更新字段列表。

  • 对于hkeys 查询是否过期 如果过期返回*0\r\n 否则返回拼接的字符串 "$" + std::to_string(field.size()) + "\r\n" + field + "\r\n";

7.4 set类型

对于set类型我们不把所有Set中所有的字符串都编码或者拼接后存储在单个键值对的value中, 因为通常的业务中, 一个Set中的字符串数量是远大于哈希结构的filed的, 这里若还采用整体编码存储在单个键值对中, 在CRUD的过程中, 编解码会消耗大量的时间。

我们直接将字符串当作有前缀的 key插入lsm

例如 命令 sadd

1
sadd nums 1 2 3 4 5

此时key为nums ,然后将字符串改变为 nums_1 nums_2 nums_3 nums_4 nums_5 ,然后按照(nums_1 ,1).。。。(nums,5)插入LSM。 get时候根据前缀和进行查询

7.5 RedisSever

使用moduo网络框架进行服务端搭建

7.5.1 bind

muduo 作为 Reactor 模型的网络库,针对 TCP 连接的不同事件,设计了不同类型的回调函数,每种回调的参数列表是库预先定义好的(这是 “函数签名” 的要求,必须匹配才能调用)。

回调类型 注册函数 作用场景 要求的函数签名(参数列表) 你的例子对应
连接回调 setConnectionCallback 处理 “连接建立 / 断开” 事件 仅 1 个参数:const TcpConnectionPtr& 你之前写的 onConnection
消息回调 setMessageCallback 处理 “客户端发送数据到达” 事件 3 个参数:const TcpConnectionPtr&Buffer*Timestamp 现在的 onMessage

简单说:

  • 当客户端刚连上来 / 刚断开时,muduo 会调用「连接回调」,此时只需要知道 “是哪个连接发生了事件”(所以 1 个参数足够);
  • 当客户端发来了数据时,muduo 会调用「消息回调」,此时需要知道 “哪个连接”“数据存在哪里”“数据是什么时候到的”(所以需要 3 个参数)。

“只能有一个参数”,是特指「连接回调」,而 onMessage 是「消息回调」,参数数量自然不同 —— 这是库的设计决定的,不是矛盾。

第二步:为什么 “对象的成员函数” 需要用 std::bind

你之前的疑问 “为啥不能传入对象的函数,只能 bind 绑定”,核心原因是:对象的成员函数比普通函数多一个 “隐藏参数”——this 指针

我们用你的 onMessage 举例子:
假设你把 onMessage 定义为某个类(比如 EchoServer)的成员函数:

1
2
3
4
5
6
7
class EchoServer {
public:
// 成员函数:隐含第一个参数是 EchoServer* (this 指针)
void onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp time) {
// 处理逻辑...
}
};

此时,EchoServer::onMessage真实函数签名是:
void (EchoServer*, const TcpConnectionPtr&, Buffer*, Timestamp)
(第一个参数是 this,指向调用该函数的 EchoServer 对象)。

但 muduo 的 setMessageCallback 要求的函数签名是:
void (const TcpConnectionPtr&, Buffer*, Timestamp)
(没有 EchoServer* 这个参数)。

两者签名不匹配,直接传 &EchoServer::onMessage 会编译报错 —— 因为 muduo 不知道要传给 this 指针哪个对象。

std::bind 的作用,就是把 “成员函数” 和 “它所属的对象” 绑定起来,消去 this 这个隐藏参数,让绑定后的函数签名和 muduo 要求的一致。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 创建一个 EchoServer 对象
EchoServer server;

// 2. 绑定:把 server 的 this 指针“固化”到 onMessage 中
auto msgCallback = std::bind(&EchoServer::onMessage,
&server, // 传给 this 指针的对象地址
std::placeholders::_1, // 对应第一个参数:conn
std::placeholders::_2, // 对应第二个参数:buf
std::placeholders::_3); // 对应第三个参数:time

// 3. 此时 msgCallback 的签名就匹配了,可以传给 setMessageCallback
server.setMessageCallback(msgCallback);

绑定后,当 muduo 调用 msgCallback(conn, buf, time) 时,底层会自动转为 server.onMessage(conn, buf, time)——this 指针已经通过 &server 确定了。

总结

  1. 参数数量差异的原因:你看到的 onMessage 是「消息回调」(3 个参数),之前的 onConnection 是「连接回调」(1 个参数),两者是 muduo 为不同事件设计的不同回调,规则独立,不矛盾;
  2. std::bind 的作用:对象的成员函数有隐藏的 this 指针,导致函数签名和 muduo 要求的不匹配;std::bind 能绑定对象地址(消去 this),让签名匹配,从而正常传入。

五、问题记录

5.1 Memtable友元类

Memtable的友元类为什么要有HeapIterator等这两个类,HeapIterator访问了Memtable里的成员吗?

1
2
3
class MemTable {
friend class TranContext;
friend class HeapIterator;

5.2 Block::add_entry

1
2
3
if(!force_write && (cur_size()+key.size()+value.size()+3*sizeof(uint16_t)+sizeof(uint64_t)>capacity) && !offsets.empty()){
return false;
}

上述代码中的!offsets.empty() 设计的意义是什么,为空 肯定能插入,就不考虑超过块大小? 不为空才考虑是否超过块大小吗? 那么不应该写在前面吗 ,写后面起不到作用呢?