vault backup: 2026-05-25 23:07:45

This commit is contained in:
hhs
2026-05-25 23:07:45 +08:00
parent 9000c90abe
commit 268ee58de6
49 changed files with 282 additions and 54 deletions
+97 -13
View File
@@ -271,25 +271,41 @@ flowchart TD
### TTL / 过期键处理
> [!QUESTION] RDB 快照会保存已过期的 key 吗?
> **不会。** 这是一个容易误解的点——很多人以为快照是"拍照片",会原封不动地保存内存里所有东西。但实际上,Redis 在"拍照"之前会先"打扫房间"。
> **最终不会。** 这是一个容易误解的点——很多人以为快照是"拍照片",会原封不动地保存内存里所有东西。实际上,Redis 通过**两道防线**确保过期键不会进入最终数据。
具体流程:
1. `BGSAVE` 触发时,主线程**先执行一次主动过期扫描**,把所有已超时的 key 从内存中删除
2. 删除完成后,才 fork 子进程
3. 子进程遍历剩余的 key → 序列化写入 `.rdb` 文件
4. 结论:**`.rdb` 文件中只有有效数据,不包含过期键**
**第一道防线:子进程遍历时惰性跳过**
BGSAVE 触发时,Redis **不会**在 fork 前专门做一次"全量过期清理"(这在数据量大时太慢了)。实际流程是:
1. 主线程 fork 子进程
2. 子进程遍历所有 key,序列化写入 `.rdb` 文件
3. 在遍历每个 key 时,子进程会**检查 TTL 是否已过期**——如果已过期,**直接跳过,不写入文件**
> [!NOTE] 那日常的过期清理呢?
> Redis 平时通过两种机制清理过期键:
> - **惰性删除**:访问 key 时发现已过期 → 立即删除
> - **定期删除**:`serverCron` 每 100ms 随机抽样一批设置了 TTL 的 key,删除其中已过期的
>
> 所以到 BGSAVE 触发时,内存中**大部分**过期键其实已经被日常清理掉了。子进程遍历时再跳过漏网之鱼,双重保障。
**第二道防线:加载时二次检查**
即使某个 key 在快照时还没过期,但等到恢复加载时已经过期了——Redis 在加载 RDB 文件时会**再次检查 TTL**,过期的 key 不会被加载到内存。
```mermaid
flowchart LR
A["BGSAVE 触发"] --> B["主线程: 主动过期扫描<br/>清理所有已超时 Key"]
B --> C["Fork 子进程"]
C --> D["子进程: 遍历有效数据<br/>序列化写入 .rdb"]
D --> E["RDB 文件中无过期键"]
style E fill:#c4e8b0
A["BGSAVE 触发"] --> B["Fork 子进程"]
B --> C["子进程遍历所有 Key"]
C --> D{"Key 是否<br/>已过期?"}
D -->|"是"| E["跳过,不写入 .rdb"]
D -->|"否"| F["序列化写入 .rdb"]
F --> G["RDB 文件只含有效数据"]
E --> C
style G fill:#c4e8b0
```
> [!NOTE] 一个细节:RDB 加载时也会二次检查
> 即使某个 key 在快照时还没过期,但等到恢复加载时已经过期了——Redis 在加载 RDB 文件时会**再次检查 TTL**,过期的 key 不会被加载到内存。这保证了无论什么时候恢复,数据都是"新鲜"的。
> [!QUESTION] 为什么要分两道防线,不能一次性清理干净?
> 因为**时间是流动的**。假设一个 key 的 TTL 还剩 1 秒时触发了 BGSAVE,子进程遍历时它还活着(写入了 .rdb),但 2 秒后加载时它已过期。所以加载时的二次检查是**必须的**——它处理的是"快照时还活着、但恢复时已过期"的时间差。
## 恢复流程
@@ -510,6 +526,74 @@ redis-check-rdb /var/lib/redis/dump.rdb
# 输出 OK 则文件完整,否则会告诉你哪里损坏了
```
## 常见故障排查
> [!WARNING] BGSAVE 失败?按这个顺序排查
> 以下是最常见的三类故障,按发生概率排序:
**1. fork 失败:`Can't save in background: fork: Cannot allocate memory`**
```bash
# 检查当前 overcommit 设置
cat /proc/sys/vm/overcommit_memory
# 如果输出 0 或 2 → 需要改为 1
# 临时修改(立即生效,重启失效)
echo 1 > /proc/sys/vm/overcommit_memory
# 永久修改(写入 sysctl)
echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf && sysctl -p
```
> [!INFO] 为什么 overcommit_memory 必须设为 1?
> fork 需要内核分配页表空间,但此时并不会真正使用等量的物理内存(COW)。`overcommit_memory=0`(默认)时,内核会用启发式算法估算——对于大内存 Redis 实例,这个估算可能过于保守而拒绝 fork。设为 1 是告诉内核"不要猜了,直接允许"。
**2. 磁盘空间不足:写入 .rdb 时报错**
```bash
# 检查 dir 目录的可用空间
df -h /var/lib/redis
# 检查实际文件大小
ls -lh /var/lib/redis/dump.rdb
# 紧急处理:临时关闭 RDB 自动触发
redis-cli CONFIG SET save ""
```
**3. RDB 文件损坏:加载时 `Short read or OOM loading DB`**
```bash
# 使用 Redis 自带工具检查
redis-check-rdb /var/lib/redis/dump.rdb
# 如果文件损坏且有备份 → 用备份替换
# 如果没有备份 → 看 AOF 是否可用(Redis 优先加载 AOF)
```
## 安全性考虑
> [!WARNING] RDB 文件是明文存储的!
> `.rdb` 文件虽然看起来是"二进制",但其中的 key 名称和字符串类型的 value 都是**明文可读**的。用 `strings dump.rdb | head` 就能看到大部分内容。
**生产环境建议**:
1. **文件权限**:确保 `.rdb` 文件权限为 `600`(仅 Redis 用户可读写)
```bash
chmod 600 /var/lib/redis/dump.rdb
```
2. **备份加密**:备份到远端存储前,先加密再上传
```bash
# 使用 openssl 加密备份
openssl enc -aes-256-cbc -salt -pbkdf2 \
-in dump.rdb -out dump.rdb.enc -pass pass:$REDIS_BACKUP_KEY
```
3. **传输加密**:跨机器复制时使用 `scp` 或 `rsync over SSH`,避免明文传输
4. **敏感数据脱敏**:如果存了密码、Token 等敏感信息,建议在应用层加密后再写入 Redis——不要依赖 Redis 自身的"安全性"
> [!TIP] Redis 企业版(Redis Enterprise)支持透明的 RDB 文件加密
> 社区版不支持原生加密。如果你有强合规需求,可以考虑企业版或者在应用层做客户端加密。
## 关联笔记
- [[hhs/Redis/05-AOF持久化]] — AOF 持久化机制