
来源:程序员老廖
本教程将手把手带你从零实现一个高性能的Mini-Redis,涵盖RESP协议解析、事件驱动网络编程、数据结构实现、持久化和主从复制等核心技术。
这个kv存储项目是可以真正写入简历的C++项目,不同于网上其它的kv存储项目,加了持久化后,QPS测试性能只能到一千多,而这个项目可以达到55000+,性能可以媲美redis。

视频讲解及源码领取:C++项目推荐-真正可以媲美redis的kv存储项目-包括性能如何逐步优化

一个完整的请求经历以下阶段:
模块 | 文件 | 职责 |
|---|---|---|
网络层 | server.cpp | epoll事件循环,TCP连接管理 |
协议层 | resp.hpp/cpp | RESP协议解析和序列化 |
存储层 | kv.hpp/cpp | 数据结构实现,过期管理 |
持久化 | aof.hpp/cpp, rdb.hpp/cpp | AOF/RDB持久化 |
复制 | replica_client.hpp/cpp | 主从复制 |
配置 | config.hpp, config_loader.cpp | 配置解析 |
# Ubuntu/Debian
sudo apt install build-essential cmake pkg-config
# CentOS/RHEL
sudo yum install gcc-c++ cmake make
# 检查版本
g++ --version # 需要支持C++17
cmake --version # 建议3.15+mkdir mini-redis && cd mini-redis
mkdir -p include/mini_redis src docs tools build项目目录结构:
mini-redis/
├── CMakeLists.txt # CMake构建文件
├── include/mini_redis/ # 头文件
│ ├── config.hpp # 配置结构体
│ ├── resp.hpp # RESP协议
│ ├── kv.hpp # KV存储
│ ├── aof.hpp # AOF持久化
│ ├── rdb.hpp # RDB持久化
│ └── replica_client.hpp # 主从复制
├── src/ # 源文件
│ ├── main.cpp # 程序入口
│ ├── server.cpp # 服务器主逻辑
│ ├── resp.cpp # RESP实现
│ ├── kv.cpp # KV存储实现
│ ├── aof.cpp # AOF实现
│ ├── rdb.cpp # RDB实现
│ ├── replica_client.cpp # 复制实现
│ └── config_loader.cpp # 配置加载
├── docs/ # 文档
├── tools/ # 工具脚本
└── build/ # 构建目录创建 CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(mini_redis)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 编译选项
set(CMAKE_CXX_FLAGS "-Wall -Wextra -g")
set(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native")
# 头文件路径
include_directories(include)
# 源文件
set(SOURCES
src/main.cpp
src/server.cpp
src/resp.cpp
src/kv.cpp
src/aof.cpp
src/rdb.cpp
src/replica_client.cpp
src/config_loader.cpp
)
# 可执行文件
add_executable(mini_redis ${SOURCES})
# 链接库
find_package(Threads REQUIRED)
target_link_libraries(mini_redis PRIVATE Threads::Threads)cd mini-redis
cmake -S . -B build
cmake --build build -jMini-Redis 支持三种持久化配置模式:
适用于:测试、缓存场景,不需要数据持久化
# 启动服务器
./build/mini_redis --config build/none.conf
# 服务器将在端口 6388 启动,无 AOF 和 RDB适用于:生产环境,平衡性能和数据安全
# 启动服务器
./build/mini_redis --config build/everysec.conf
# 服务器将在端口 6388 启动,AOF 每秒同步一次适用于:对数据一致性要求极高的场景
# 启动服务器
./build/mini_redis --config build/always.conf
# 服务器将在端口 6388 启动,每个写操作立即同步到磁盘port=6388
aof.enabled=false
rdb.enabled=falseport=6388
aof.enabled=true
aof.mode=everysec
aof.dir=./data
aof.filename=appendonly.aof
rdb.enabled=false
aof.batch_bytes=262144
aof.batch_wait_us=2000
aof.prealloc_bytes=67108864
aof.sync_interval_ms=250port=6388
aof.enabled=true
aof.mode=always
aof.dir=./data
aof.filename=appendonly.aof
rdb.enabled=false创建主节点配置文件 master.conf:
port=6379
bind_address=0.0.0.0
# AOF 持久化
aof.enabled=true
aof.mode=everysec
aof.dir=./data-master
aof.filename=appendonly.aof
# RDB 快照
rdb.enabled=true
rdb.dir=./data-master
rdb.filename=dump.rdb启动主节点:
./build/mini_redis --config master.conf创建从节点配置文件 replica.conf:
port=6380
bind_address=0.0.0.0
# RDB 用于接收主节点快照
rdb.enabled=true
rdb.dir=./data-replica
rdb.filename=dump.rdb
# 从节点一般不开启 AOF
aof.enabled=false
# 复制配置
replica.enabled=true
replica.master_host=127.0.0.1
replica.master_port=6379启动从节点:
./build/mini_redis --config replica.conf# 连接到单机模式
redis-cli -p 6388
# 连接到主节点
redis-cli -p 6379
# 连接到从节点
redis-cli -p 6380# 测试连接
redis-cli -p 6388 PING
# 获取服务器信息
redis-cli -p 6388 INFO
# 回显测试
redis-cli -p 6388 ECHO "Hello Mini-Redis"# 设置键值
redis-cli -p 6388 SET mykey "Hello World"
# 获取值
redis-cli -p 6388 GET mykey
# 删除键
redis-cli -p 6388 DEL mykey
# 设置过期时间(秒)
redis-cli -p 6388 SET tempkey "temporary"
redis-cli -p 6388 EXPIRE tempkey 60
# 查看剩余过期时间
redis-cli -p 6388 TTL tempkey
# 检查键是否存在
redis-cli -p 6388 EXISTS mykey# 设置 Hash 字段
redis-cli -p 6388 HSET user:1 name "Alice"
redis-cli -p 6388 HSET user:1 age "25"
redis-cli -p 6388 HSET user:1 city "Beijing"
# 获取 Hash 字段
redis-cli -p 6388 HGET user:1 name
# 获取所有字段和值
redis-cli -p 6388 HGETALL user:1
# 检查字段是否存在
redis-cli -p 6388 HEXISTS user:1 email
# 删除字段
redis-cli -p 6388 HDEL user:1 age
# 获取字段数量
redis-cli -p 6388 HLEN user:1# 添加成员和分数
redis-cli -p 6388 ZADD leaderboard 100 "player1"
redis-cli -p 6388 ZADD leaderboard 85 "player2"
redis-cli -p 6388 ZADD leaderboard 92 "player3"
# 按分数范围查询(默认升序)
redis-cli -p 6388 ZRANGE leaderboard 0 -1
# 按分数范围查询并显示分数
redis-cli -p 6388 ZRANGE leaderboard 0 -1 WITHSCORES
# 获取成员分数
redis-cli -p 6388 ZSCORE leaderboard "player2"
# 删除成员
redis-cli -p 6388 ZREM leaderboard "player2"# 列出所有键
redis-cli -p 6388 KEYS "*"
# 清空所有数据
redis-cli -p 6388 FLUSHALL
# 触发 RDB 快照保存
redis-cli -p 6388 BGSAVE# 连接主节点并写入
redis-cli -p 6379 SET repl:test "master-data"
redis-cli -p 6379 HSET repl:hash field1 "value1"
redis-cli -p 6379 ZADD repl:zset 90 "item1"# 连接从节点并读取
redis-cli -p 6380 GET repl:test
redis-cli -p 6380 HGETALL repl:hash
redis-cli -p 6380 ZRANGE repl:zset 0 -1 WITHSCORES# 创建测试数据文件
echo -e "SET key1 value1\nSET key2 value2\nSET key3 value3" > test-commands.txt
# 通过管道执行
redis-cli -p 6388 --pipe < test-commands.txt# 基本性能测试
redis-benchmark -h 127.0.0.1 -p 6388 -n 10000 -c 50
# 测试 SET 操作
redis-benchmark -h 127.0.0.1 -p 6388 -t set -n 10000 -d 100
# 测试 GET 操作
redis-benchmark -h 127.0.0.1 -p 6388 -t get -n 10000这几个章节需要搭配源码一起看,详细的学习文档和源码都在这个视频讲解中给出来,大家可以去观看领取
使用redis-benchmark进行性能测试,对比优化前后的效果:
# 基准测试命令
redis-benchmark -h 127.0.0.1 -p 6379 -n 100000 -c 50 -P 10 -t set,get
redis-benchmark -h 127.0.0.1 -p 6379 -n 50000 -c 10 -P 1 -d 1000 -t set,get优化项 | 优化前QPS | 优化后QPS | 提升倍数 | 备注 |
|---|---|---|---|---|
基础实现 | 15k | - | - | 阻塞I/O + 同步AOF |
非阻塞I/O + epoll | 15k | 45k | 3x | 事件驱动 |
边沿触发(EPOLLET) | 45k | 52k | 1.15x | 减少系统调用 |
writev批量发送 | 52k | 58k | 1.12x | 减少网络系统调用 |
AOF异步写入 | 1.4k | 55k | 39x | AOF模式下的巨大提升 |
Group Commit | 55k | 48k | 0.87x | always模式权衡 |
最关键的优化是AOF的异步写入机制:
// 优化前:同步写入(性能杀手)
void appendAOF_slow(const std::string& cmd) {
std::ofstream file(aof_path_, std::ios::app);
file << cmd;
file.flush(); // 立即刷盘,QPS暴跌
}
// 优化后:异步批量写入
void appendAOF_fast(const std::string& cmd) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
aof_queue_.push(cmd);
pending_bytes_ += cmd.size();
}
cv_.notify_one(); // 唤醒后台写入线程
}// 优化技巧1:TCP_NODELAY避免Nagle算法延迟
int opt = 1;
setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
// 优化技巧2:writev批量发送
struct iovec iov[64]; // 增大到64个iovec
int count = 0;
for (const auto& chunk : out_chunks) {
iov[count].iov_base = (void*)chunk.data();
iov[count].iov_len = chunk.size();
if (++count >= 64) break;
}
writev(fd, iov, count);
// 优化技巧3:边沿触发一次性读完
while (true) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n <= 0) {
if (errno == EAGAIN) break; // 读完了
// 处理错误
}
process_data(buf, n);
}// 小集合用vector,大集合用跳表
class ZSetRecord {
static constexpr size_t THRESHOLD = 128;
std::vector<std::pair<double, std::string>> small_set; // <128元素
std::unique_ptr<Skiplist> skiplist; // >=128元素
void checkAndConvert() {
if (!use_skiplist && small_set.size() >= THRESHOLD) {
convertToSkiplist(); // 自动升级
}
}
};// 预分配AOF文件空间,避免频繁扩展
void preallocateAOF(int fd, size_t size) {
if (posix_fallocate(fd, 0, size) == 0) {
printf("Preallocated %zu bytes for AOF\n", size);
}
}
// 连接对象复用
class ConnectionPool {
std::vector<std::unique_ptr<Conn>> free_conns_;
std::unique_ptr<Conn> acquire() {
if (!free_conns_.empty()) {
auto conn = std::move(free_conns_.back());
free_conns_.pop_back();
conn->reset(); // 重置状态
return conn;
}
return std::make_unique<Conn>();
}
};原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。