RDB (Redis Database) 是 Redis 的核心持久化机制之一它的核心思想是在指定的时间间隔为内存中的数据生成一份时间点快照Point-in-time Snapshot并保存到一个紧凑的二进制文件中默认为 dump.rdb。它的优点和缺点都很鲜明恢复速度快、对性能影响小但也伴随着数据丢失风险高、生成时资源消耗大的不足。核心机制Copy-on-Write写时复制1、父进程fork()子进程当需要生成 RDB 时Redis 主进程会调用fork()系统调用创建一个子进程。在这个瞬间子进程会复制父进程的页表两者共享同一份物理内存数据。fork 完成后内存不是彻底隔离的是“一开始共享写时才复制。// 假设 Redis 有 4GB 内存数据 // fork 之前 父进程: [数据页1][数据页2][数据页3]...[数据页N] 物理内存 4GB // 执行 fork() 瞬间 1. 操作系统创建子进程 2. 复制父进程的页表不是数据 3. 父子进程的页表指向同一物理内存 父进程页表: [页1→物理地址A][页2→物理地址B][页3→物理地址C] 子进程页表: [页1→物理地址A][页2→物理地址B][页3→物理地址C] ↓ 同一块物理内存 (4GB) 用户程序调用 fork() ↓ 陷入内核态系统调用 ↓ 1. 复制进程描述符task_struct ↓ 2. 复制内存描述符mm_struct ↓ 3. 复制页表共享物理内存 ↓ 4. 复制文件描述符表 ↓ 5. 复制信号处理表 ↓ 6. 分配新的 PID ↓ 7. 子进程加入调度队列 ↓ 返回用户态父进程返回 PID子进程返回 0父进程修改数据/新增数据父进程要修改物理页时会先检查该页是否被共享物理页引用计数如果该页被共享会执行写时复制1分配新的物理页给父进程2操作系统复制旧的物理页内存到新的物理页上3父进程页表指向新的物理页子进程页表仍指向旧的物理页4旧的物理页减少引用计数。如果子进程要修改数据也是一样修改完后两个物理页彻底隔离。2、子进程读、写磁盘子进程负责将共享内存中的所有数据序列化并写入一个临时的 RDB 文件中。由于它持有的是 fork 时刻的内存快照所以看到的数据是一致且不变的。3、父进程继续处理新请求父进程不受干扰继续处理客户端请求。当有写请求发生时操作系统会为被修改的内存页创建一个副本父进程在副本上修改而子进程依然读取原始的内存页来生成快照。4、原子替换当子进程完成临时文件的写入后会用这个临时文件原子地替换旧的 RDB 文件完成一次持久化操作。这个机制确保了 Redis 主进程在备份期间几乎不会被阻塞性能影响极小。RDB文件结构文件头Header固定以 REDIS 5 个字节开头用于快速校验文件类型。紧接着是 4 个字节的版本号如0007以字符串形式存储。文件的主体包含了实际的数据。如果某个数据库为空则不会在此部分出现。每个非空数据库部分都以一个SELECTDB操作码开头随后紧跟数据库编号如0,1最后是具体的键值对Key-Value Pairs数据。文件尾是一个特定的字节EOF0xFF标记键值对数据的结束最后加上一个 8 字节的 CRC64 校验和用于在加载时验证文件是否完整或损坏。RDB 的优缺点优点1恢复速度快RDB 是数据的二进制快照恢复时直接加载到内存比 AOF 逐条重放命令要快得多。2文件紧凑便于备份生成的dump.rdb文件体积小非常适合做冷备或异地容灾。3对性能影响小持久化过程主要消耗在子进程父进程几乎无额外 I/O 开销。缺点1数据丢失风险由于是间隔性备份如果 Redis 在两次快照之间意外宕机最后一次快照之后的所有修改都将丢失。2fork 资源消耗当数据集非常大时fork()子进程本身会消耗一定时间并导致内存占用瞬时翻倍页表复制在高负载场景下可能引发延迟。总的来说RDB 是一种非常适合备份和灾难恢复的机制它的高性能和快速恢复特性使其在生产环境中备受青睐。但为了平衡数据安全性与性能官方建议将 RDB 和 AOF 两种持久化方式结合使用。如果同时启用Redis 在重启时会优先使用 AOF 文件来恢复数据因为它通常能提供更完整的数据。为什么不使用读写锁而使用fork()?pthread_rwlock_t config_lock; // 读线程频繁读取配置 void* worker_thread(void* arg) { while (1) { pthread_rwlock_rdlock(config_lock); int timeout g_config.timeout; // 多个线程可并发读 int max_conn g_config.max_connections; pthread_rwlock_unlock(config_lock); // 使用配置工作... do_work(timeout, max_conn); } } // 管理线程偶尔更新配置 void* config_reloader(void* arg) { while (1) { sleep(60); // 每分钟重载一次 Config new_config; load_from_file(new_config); pthread_rwlock_wrlock(config_lock); // 独占锁 g_config new_config; // 更新配置 pthread_rwlock_unlock(config_lock); } }读写锁可以允许多个线程同时获取读锁适用于读多写少的场景。而redis的使用场景读写基本不固定出现大量读操作时会出现大量的锁竞争造成性能延时抖动。读写锁使用场景1配置管理配置多是读多写少。2缓存系统读多写少。3路由表、DNS缓存。4数据库连接池读多写少。5黑白名单。读写操作通过同一把锁对象如std::shared_mutex竞争在某些场景下可能出现写锁饥饿或读锁延迟过高的问题性能不稳定。问题表现原因写锁饥饿写线程长时间无法获得锁新读锁不断插队读锁延迟读线程在有写等待时被阻塞某些实现的写优先策略性能抖动吞吐量忽高忽低读/写模式切换读写锁优化方案方案1使用公平读写锁#include mutex #include condition_variable class FairRWLock { std::mutex mtx; std::condition_variable cv; int readers 0; bool writing false; int waiting_writers 0; public: void read_lock() { std::unique_lock lock(mtx); // 如果有人在等写锁读也要等公平 cv.wait(lock, [this] { return !writing waiting_writers 0; }); readers; } void write_lock() { std::unique_lock lock(mtx); waiting_writers; cv.wait(lock, [this] { return !writing readers 0; }); waiting_writers--; writing true; } void unlock() { std::unique_lock lock(mtx); if (writing) { writing false; } else { readers--; } cv.notify_all(); } };方案2减少锁竞争RCU// Read-Copy-Update 思想 templatetypename T class RCUProtected { std::atomicT* ptr; std::shared_mutex update_mtx; public: T* read() { return ptr.load(); // 无锁读 } void update(T* new_ptr) { std::unique_lock lock(update_mtx); // 只有更新需要锁 T* old ptr.exchange(new_ptr); // 等待所有读完成RCU 宽限期 std::this_thread::sleep_for(std::chrono::milliseconds(100)); delete old; } };方案3读写分离双缓冲templatetypename T class DoubleBuffer { T buffer[2]; std::atomicint read_index{0}; std::mutex write_mtx; public: T read() { return buffer[read_index.load()]; // 无锁读 } void write(const T new_value) { std::lock_guard lock(write_mtx); int write_index 1 - read_index.load(); buffer[write_index] new_value; read_index.store(write_index); // 原子切换 } };方案4限定写锁频率class BatchingRWLock { std::shared_mutex mtx; std::atomicint pending_writes{0}; const int BATCH_SIZE 100; public: void write() { if (pending_writes % BATCH_SIZE 0) { std::unique_lock lock(mtx); // 实际写操作 } else { // 累积批量写 } } void read() { std::shared_lock lock(mtx); // 读操作 } };现代优化写时复制 大页内存1.透明大页THP# 启用大页2MB echo always /sys/kernel/mm/transparent_hugepage/enabled # 效果 # 4GB 内存 # 4KB 页面1M 个 PTE → fork 慢 # 2MB 页面2K 个 PTE → fork 快 500 倍2.内核优化// 使用 RCURead-Copy-Update // 某些场景避免复制整个 mm_struct // 使用引用计数共享 // 直到写时才复制Lazy Copy #include stdio.h #include unistd.h #include sys/resource.h int main() { // 获取页大小 long page_size sysconf(_SC_PAGESIZE); printf(Page size: %ld bytes\n, page_size); // 分配内存 size_t size 100 * 1024 * 1024; // 100MB char *data malloc(size); memset(data, 0xAA, size); // 查看内存映射 printf(Before fork:\n); char cmd[100]; snprintf(cmd, sizeof(cmd), cat /proc/%d/smaps | grep -E ^(Size|Pss|Rss), getpid()); system(cmd); pid_t pid fork(); if (pid 0) { // 子进程 printf(\nChild process (PID %d):\n, getpid()); printf(Child sees same memory (COW)\n); // 修改内存触发 COW data[0] 0xBB; printf(After child modifies 1 byte:\n); system(cmd); sleep(1); } else { // 父进程 sleep(2); printf(\nParent process (PID %d):\n, getpid()); printf(Parents memory unchanged:\n); system(cmd); } return 0; }