From 268ee58de6335825399d82bffb14fa313a0d5610 Mon Sep 17 00:00:00 2001
From: hhs <386998068@qq.com>
Date: Mon, 25 May 2026 23:07:45 +0800
Subject: [PATCH] vault backup: 2026-05-25 23:07:45
---
.../docs => CHORE}/一文吃透Redis.md | 0
.../docs/Go语言web开发/01-HTTP概述.md | 0
.../docs/Go语言web开发/02-gin框架基础.md | 0
.../docs/Go语言web开发/03-服务端鉴权认证方案.md | 0
.../某公司文档/docs/一张图讲全栈.md | 0
.../picture/Pasted image 20260513135337.png | Bin
.../picture/Pasted image 20260513135604.png | Bin
.../picture/Pasted image 20260513135631.png | Bin
.../picture/Pasted image 20260513140344.png | Bin
.../picture/Pasted image 20260513140419.png | Bin
.../picture/Pasted image 20260513140526.png | Bin
.../picture/Pasted image 20260513140605.png | Bin
.../picture/Pasted image 20260513140627.png | Bin
.../picture/Pasted image 20260513140815.png | Bin
.../picture/Pasted image 20260513140905.png | Bin
.../picture/Pasted image 20260513140931.png | Bin
.../picture/Pasted image 20260513140959.png | Bin
.../picture/Pasted image 20260513141057.png | Bin
.../picture/Pasted image 20260513141135.png | Bin
.../picture/Pasted image 20260513141153.png | Bin
.../picture/Pasted image 20260513141215.png | Bin
.../picture/Pasted image 20260513141244.png | Bin
.../picture/Pasted image 20260513141303.png | Bin
.../picture/Pasted image 20260513141334.png | Bin
.../picture/Pasted image 20260513141402.png | Bin
.../picture/Pasted image 20260513141428.png | Bin
.../picture/Pasted image 20260513141457.png | Bin
.../picture/Pasted image 20260518145617.png | Bin
.../picture/Pasted image 20260518145835.png | Bin
.../picture/Pasted image 20260518145842.png | Bin
.../picture/Pasted image 20260518145921.png | Bin
.../picture/Pasted image 20260518145931.png | Bin
.../picture/Pasted image 20260518150018.png | Bin
.../picture/Pasted image 20260518151950.png | Bin
.../picture/Pasted image 20260518152001.png | Bin
.../picture/Pasted image 20260518152048.png | Bin
.../picture/Pasted image 20260518152059.png | Bin
.../picture/Pasted image 20260518152142.png | Bin
.../picture/Pasted image 20260518171622.png | Bin
.../picture/Pasted image 20260518172210.png | Bin
.../picture/Pasted image 20260518172239.png | Bin
.../picture/Pasted image 20260518172321.png | Bin
.../picture/Pasted image 20260518172400.png | Bin
.../picture/Pasted image 20260518172431.png | Bin
hhs/Redis/04-RDB持久化.md | 110 +++++++++++++--
hhs/Redis/05-AOF持久化.md | 127 +++++++++++++++++-
hhs/Redis/06-主从与哨兵.md | 10 +-
hhs/Redis/07-集群方案.md | 22 ++-
hhs/Redis/08-SortedSet精解.md | 67 +++++----
49 files changed, 282 insertions(+), 54 deletions(-)
rename hhs/{某公司文档/docs => CHORE}/一文吃透Redis.md (100%)
rename hhs/{ => CHORE}/某公司文档/docs/Go语言web开发/01-HTTP概述.md (100%)
rename hhs/{ => CHORE}/某公司文档/docs/Go语言web开发/02-gin框架基础.md (100%)
rename hhs/{ => CHORE}/某公司文档/docs/Go语言web开发/03-服务端鉴权认证方案.md (100%)
rename hhs/{ => CHORE}/某公司文档/docs/一张图讲全栈.md (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513135337.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513135604.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513135631.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140344.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140419.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140526.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140605.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140627.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140815.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140905.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140931.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513140959.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141057.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141135.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141153.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141215.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141244.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141303.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141334.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141402.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141428.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260513141457.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518145617.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518145835.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518145842.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518145921.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518145931.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518150018.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518151950.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518152001.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518152048.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518152059.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518152142.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518171622.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518172210.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518172239.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518172321.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518172400.png (100%)
rename hhs/{ => CHORE}/某公司文档/picture/Pasted image 20260518172431.png (100%)
diff --git a/hhs/某公司文档/docs/一文吃透Redis.md b/hhs/CHORE/一文吃透Redis.md
similarity index 100%
rename from hhs/某公司文档/docs/一文吃透Redis.md
rename to hhs/CHORE/一文吃透Redis.md
diff --git a/hhs/某公司文档/docs/Go语言web开发/01-HTTP概述.md b/hhs/CHORE/某公司文档/docs/Go语言web开发/01-HTTP概述.md
similarity index 100%
rename from hhs/某公司文档/docs/Go语言web开发/01-HTTP概述.md
rename to hhs/CHORE/某公司文档/docs/Go语言web开发/01-HTTP概述.md
diff --git a/hhs/某公司文档/docs/Go语言web开发/02-gin框架基础.md b/hhs/CHORE/某公司文档/docs/Go语言web开发/02-gin框架基础.md
similarity index 100%
rename from hhs/某公司文档/docs/Go语言web开发/02-gin框架基础.md
rename to hhs/CHORE/某公司文档/docs/Go语言web开发/02-gin框架基础.md
diff --git a/hhs/某公司文档/docs/Go语言web开发/03-服务端鉴权认证方案.md b/hhs/CHORE/某公司文档/docs/Go语言web开发/03-服务端鉴权认证方案.md
similarity index 100%
rename from hhs/某公司文档/docs/Go语言web开发/03-服务端鉴权认证方案.md
rename to hhs/CHORE/某公司文档/docs/Go语言web开发/03-服务端鉴权认证方案.md
diff --git a/hhs/某公司文档/docs/一张图讲全栈.md b/hhs/CHORE/某公司文档/docs/一张图讲全栈.md
similarity index 100%
rename from hhs/某公司文档/docs/一张图讲全栈.md
rename to hhs/CHORE/某公司文档/docs/一张图讲全栈.md
diff --git a/hhs/某公司文档/picture/Pasted image 20260513135337.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513135337.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513135337.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513135337.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513135604.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513135604.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513135604.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513135604.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513135631.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513135631.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513135631.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513135631.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140344.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140344.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140344.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140344.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140419.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140419.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140419.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140419.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140526.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140526.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140526.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140526.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140605.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140605.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140605.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140605.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140627.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140627.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140627.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140627.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140815.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140815.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140815.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140815.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140905.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140905.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140905.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140905.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140931.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140931.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140931.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140931.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513140959.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513140959.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513140959.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513140959.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141057.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141057.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141057.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141057.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141135.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141135.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141135.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141135.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141153.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141153.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141153.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141153.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141215.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141215.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141215.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141215.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141244.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141244.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141244.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141244.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141303.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141303.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141303.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141303.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141334.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141334.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141334.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141334.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141402.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141402.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141402.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141402.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141428.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141428.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141428.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141428.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260513141457.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260513141457.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260513141457.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260513141457.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518145617.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518145617.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518145617.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518145617.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518145835.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518145835.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518145835.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518145835.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518145842.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518145842.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518145842.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518145842.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518145921.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518145921.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518145921.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518145921.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518145931.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518145931.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518145931.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518145931.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518150018.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518150018.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518150018.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518150018.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518151950.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518151950.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518151950.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518151950.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518152001.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518152001.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518152001.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518152001.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518152048.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518152048.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518152048.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518152048.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518152059.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518152059.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518152059.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518152059.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518152142.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518152142.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518152142.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518152142.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518171622.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518171622.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518171622.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518171622.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518172210.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518172210.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518172210.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518172210.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518172239.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518172239.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518172239.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518172239.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518172321.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518172321.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518172321.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518172321.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518172400.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518172400.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518172400.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518172400.png
diff --git a/hhs/某公司文档/picture/Pasted image 20260518172431.png b/hhs/CHORE/某公司文档/picture/Pasted image 20260518172431.png
similarity index 100%
rename from hhs/某公司文档/picture/Pasted image 20260518172431.png
rename to hhs/CHORE/某公司文档/picture/Pasted image 20260518172431.png
diff --git a/hhs/Redis/04-RDB持久化.md b/hhs/Redis/04-RDB持久化.md
index ff21b46..549451f 100644
--- a/hhs/Redis/04-RDB持久化.md
+++ b/hhs/Redis/04-RDB持久化.md
@@ -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["主线程: 主动过期扫描
清理所有已超时 Key"]
- B --> C["Fork 子进程"]
- C --> D["子进程: 遍历有效数据
序列化写入 .rdb"]
- D --> E["RDB 文件中无过期键"]
- style E fill:#c4e8b0
+ A["BGSAVE 触发"] --> B["Fork 子进程"]
+ B --> C["子进程遍历所有 Key"]
+ C --> D{"Key 是否
已过期?"}
+ 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 持久化机制
diff --git a/hhs/Redis/05-AOF持久化.md b/hhs/Redis/05-AOF持久化.md
index a7af560..87a9f38 100644
--- a/hhs/Redis/05-AOF持久化.md
+++ b/hhs/Redis/05-AOF持久化.md
@@ -67,12 +67,17 @@ appendfsync everysec # 每秒 fsync <- 推荐(最佳平衡)
| 策略 | fsync 时机 | 丢失风险 | 吞吐量 (ops/s) | 适用场景 |
|------|-----------|---------|---------------|---------|
-| `always` | 每条写命令后 | **零丢失** | ~900 | 金融级要求(极少用) |
+| `always` | 每条写命令后 | **零丢失** | ~900(注) | 金融级要求(极少用) |
| `everysec` | 每秒一次(后台线程) | 最多丢 ~1 秒 | ~74k | **生产标准配置** |
| `no` | 从不主动 fsync,交给 OS | OS 决定,可能丢 30 秒+ | ~93k | 可接受数据丢失的场景 |
+> [!note] 吞吐量数据来源
+> 以上数据来自 Redis 官方基准测试(`redis-benchmark`,HDD 磁盘)。实际生产环境中,`always` 在 SSD 上可达数万 ops/s,`everysec` 可达十万级。**具体性能以实际压测为准**,这里主要体现三者之间的相对差距。
+
> [!warning] everysec "最多丢 1 秒" 是有前提条件的
-> 这个说法成立的前提是 `fsync()` 能在 1 秒内完成。如果磁盘 IO 阻塞(比如同一块盘上跑着其他 IO 密集任务),一次 `fsync()` 可能卡住几秒甚至十几秒——此时未 fsync 的数据量会持续累积,丢失窗口也随之扩大。不过好在 Redis 的 `everysec` 模式下,`fsync()` 在**后台线程**执行,不会阻塞主线程处理新请求。
+> 这个说法成立的前提是 `fsync()` 能在 1 秒内完成。如果磁盘 IO 阻塞(比如同一块盘上跑着其他 IO 密集任务),一次 `fsync()` 可能卡住几秒甚至十几秒——此时未 fsync 的数据量会持续累积,丢失窗口也随之扩大。
+>
+> 补充一个实现细节:`everysec` 默认在**主线程**执行 `fsync()`,只有当 Redis 检测到上次 `fsync()` 耗时超过 2 秒时,才会自动降级到**后台线程**执行——这样避免长时间 fsync 阻塞主线程,但代价是主线程在降级期间的写入数据安全性进一步降低。
> [!tip] 为什么 always 这么慢?
> `always` 是在**主线程**中同步调用 `fsync()`。一次 `fsync()` 耗时约 0.2~2ms(取决于磁盘),这意味着每条写命令都要等磁盘确认,吞吐量直接从万级跌到百级——这就是"数据安全 vs 性能"的经典权衡。
@@ -122,6 +127,23 @@ auto-aof-rewrite-min-size 64mb # AOF 最小绝对大小(避免刚启动就
>
> 这本质上是一个「文件瘦身频率」的工程权衡题。
+### 手动触发重写
+
+除了自动触发,运维中经常需要手动重写——比如刚做了一次大批量数据导入,AOF 文件暴涨。
+
+```bash
+# 手动触发 AOF 重写(后台异步执行,不阻塞主线程)
+BGREWRITEAOF
+
+# 查看重写是否正在进行
+INFO persistence
+# aof_rewrite_in_progress: 1 ← 正在重写中
+# aof_rewrite_scheduled: 0 ← 是否有排队中的重写请求
+```
+
+> [!warning] 重写互斥
+> 和 BGSAVE 一样,Redis 同一时刻**只允许一个子进程**做持久化。如果 BGSAVE 正在执行,`BGREWRITEAOF` 会排队等待(`aof_rewrite_scheduled: 1`),等 BGSAVE 完成后自动触发。
+
### 重写过程(类似 RDB fork)
重写并不是简单地"删减旧 AOF",而是**从内存中的真实数据出发,生成全新的精简 AOF 文件**。整个过程分三步:
@@ -171,7 +193,7 @@ Redis 7.x 的多文件目录结构:
```bash
ls /var/lib/redis/appendonlydir/
# total 256M
-# manifest.aof <- 文件清单(记录哪些文件组成完整 AOF)
+# appendonly.aof.manifest <- 文件清单(记录哪些文件组成完整 AOF)
# base.rdb <- 最近一次重写生成的 RDB 全量快照
# incr-0000000000000003.aof <- 增量 AOF(记录重写之后的新写操作)
```
@@ -179,10 +201,43 @@ ls /var/lib/redis/appendonlydir/
> [!question] 为什么 Redis 7 要把单文件拆成目录结构?
> 旧版单个 AOF 文件有一个痛点:重写时需要先生成临时文件,再 `rename` 替换。如果 AOF 有 50GB,rename 操作虽然原子但磁盘 IO 开销巨大。拆成多文件后,重写只需替换 `base.rdb` 和新增 `incr-*.aof`,IO 开销大幅降低,也更方便做增量备份。
+### AOF 关键配置参数汇总
+
+除了前面讲过的 `appendfsync`,还有几个配置项直接影响 AOF 的行为和稳定性:
+
+```conf
+# ---- AOF 核心参数 ----
+appendonly yes # 开启 AOF
+appendfilename "appendonly.aof" # AOF 文件名
+appendfsync everysec # 刷新策略(always / everysec / no)
+
+# ---- 重写相关 ----
+auto-aof-rewrite-percentage 100 # AOF 增长百分比触发重写
+auto-aof-rewrite-min-size 64mb # AOF 最小绝对大小才触发重写
+no-appendfsync-on-rewrite no # 重写期间是否暂停 fsync(见下方说明)
+
+# ---- 容错相关 ----
+aof-load-truncated yes # 启动加载时是否允许截断末尾损坏的命令
+aof-use-rdb-preamble yes # 重写时是否使用 RDB 作为前缀(混合持久化开关)
+
+# ---- Redis 7 新增 ----
+aof-timestamp-enable no # 是否在 AOF 中记录时间戳(用于 PITR)
+aof-ignore-errors no # fsync 失败时是否忽略错误继续运行
+```
+
+| 参数 | 推荐值 | 要点 |
+|------|--------|------|
+| `no-appendfsync-on-rewrite` | `no` | 设为 `yes` 时,AOF 重写期间暂停 fsync 以减少磁盘争抢,但代价是**重写期间的数据安全性降级为 `no` 策略**——只在磁盘 IO 严重瓶颈时考虑 |
+| `aof-load-truncated` | `yes` | 设为 `yes` 时,Redis 启动发现 AOF 末尾不完整会自动丢弃末尾损坏命令并继续加载。设为 `no` 则直接报错退出——更安全但需要人工介入修复 |
+| `aof-timestamp-enable` | 按需 | Redis 7 的实验性功能,开启后 AOF 中会嵌入时间戳注释,配合 `redis-check-aof --timestamp` 可实现**按时间点恢复(PITR)**——比如"恢复到今天 14:30 的状态"。目前仍是实验特性,生产环境谨慎使用 |
+
+> [!question] `no-appendfsync-on-rewrite` 什么时候该开?
+> 当你观察到 AOF 重写期间主线程出现明显延迟(`redis-cli --latency` 抖动),且 `INFO persistence` 显示 `aof_delayed_fsync` 持续增长时,可以考虑临时开启。这相当于用**重写期间的数据安全换性能**,是一把双刃剑。
+
## 混合持久化(Hybrid Persistence)
> [!tip] 混合持久化是 AOF 重写的"终极形态"
-> Redis 4.0 引入、Redis 7 默认开启。核心思路:AOF **重写**时先写 RDB 全量快照作为"底子",再追加增量 AOF 命令作为"补丁"——恢复时先加载 RDB(秒级),再回放少量增量 AOF(补齐数据)。
+> Redis 4.0 引入、Redis 5.0 开始默认开启(`aof-use-rdb-preamble yes`)。核心思路:AOF **重写**时先写 RDB 全量快照作为"底子",再追加增量 AOF 命令作为"补丁"——恢复时先加载 RDB(秒级),再回放少量增量 AOF(补齐数据)。
>
> **在 AOF 视角下**:重写后生成的文件不再全是文本命令,而是 `RDB 二进制基线 + AOF 增量日志` 的混合结构。好处是恢复速度快(接近纯 RDB)且数据安全性高(接近纯 AOF)。
>
@@ -217,7 +272,11 @@ ls /var/lib/redis/appendonlydir/
redis-check-aof --fix appendonly.aof
# 场景三:Redis 7 多文件目录结构
-redis-check-aof --fix appendonlydir/appendonly.aof.1.incr.aof
+# 先查看 manifest 了解文件组成
+cat /var/lib/redis/appendonlydir/appendonly.aof.manifest
+# 修复具体的增量文件(找到损坏的那个)
+redis-check-aof --fix /var/lib/redis/appendonlydir/appendonly.aof.1.incr.aof
+# 注意:base.rdb 损坏需要用 redis-check-rdb 修复
```
> [!tip] 实际修复流程
@@ -233,14 +292,19 @@ flowchart TD
FS["fsync() 调用"] --> OK{"返回成功?"}
OK -->|"是"| CONT["继续正常服务"]
OK -->|"否:EIO 磁盘错误"| LOG["记录日志
I/O error writing to APPEND ONLY FILE"]
- LOG --> SD["执行 SHUTDOWN NOSAVE
立即停止服务"]
+ LOG --> CFG{"aof-ignore-errors
配置?"}
+ CFG -->|"no (默认)"| SD["执行 SHUTDOWN NOSAVE
立即停止服务"]
+ CFG -->|"yes (Redis 7+)"| IGN["忽略错误,继续服务
但数据可能丢失"]
style SD fill:#fdd,stroke:#c00
+ style IGN fill:#ffd,stroke:#c90
style CONT fill:#dfd,stroke:#090
```
> [!note] Redis 为什么会主动 shutdown?
-> 这是一种「fail-fast」设计哲学。AOF 的核心价值就是数据安全,当底层磁盘出现问题时,继续运行只会导致更多数据静默损坏。**宁可停机也不能让用户在"不确定"的数据上继续业务**——这和数据库遇到 WAL 写入失败时的处理逻辑是一样的。
+> 默认行为是一种「fail-fast」设计哲学。AOF 的核心价值就是数据安全,当底层磁盘出现问题时,继续运行只会导致更多数据静默损坏。**宁可停机也不能让用户在"不确定"的数据上继续业务**——这和数据库遇到 WAL 写入失败时的处理逻辑是一样的。
+>
+> Redis 7 引入了 `aof-ignore-errors` 配置项,设为 `yes` 时 Redis 会忽略 AOF 写入错误继续运行。但除非你完全理解后果(数据静默丢失),否则**不建议开启**。
> [!warning] AOF 不是万能的
> - AOF 修复工具只能处理末尾截断,中间损坏只能截断丢失后面的数据
@@ -267,6 +331,55 @@ flowchart TD
> [!TIP] 生产环境黄金组合
> `AOF everysec + 混合持久化 + 定时 rdb 冷备到对象存储`——几乎覆盖所有常见场景。
+## 运维速查手册
+
+### AOF 监控关键指标
+
+```bash
+INFO persistence
+```
+
+重点关注以下字段:
+
+| 指标 | 含义 | 告警建议 |
+|------|------|----------|
+| `aof_enabled` | AOF 是否开启 | 应为 `1` |
+| `aof_rewrite_in_progress` | 是否正在重写 | 长时间为 `1` 需排查 |
+| `aof_rewrite_scheduled` | 是否有排队中的重写 | 长时间为 `1` 说明重写被阻塞 |
+| `aof_last_bgrewrite_status` | 上次重写是否成功 | ≠ `ok` 立即告警 |
+| `aof_last_write_status` | 上次 AOF 写入是否成功 | ≠ `ok` 说明磁盘可能有问题 |
+| `aof_current_size` | 当前 AOF 文件大小 | 持续膨胀未触发重写需检查配置 |
+| `aof_base_size` | 上次重写后 AOF 大小 | 结合 `aof_current_size` 计算增长比 |
+| `aof_delayed_fsync` | 被延迟的 fsync 次数 | 持续增长说明磁盘 IO 成为瓶颈 |
+
+> [!question] 如何快速判断 AOF 是否健康?
+> 一个简单公式:`aof_current_size / aof_base_size`。如果比值远大于 `auto-aof-rewrite-percentage` 的配置值(默认 2.0),说明自动重写可能没有正常触发——检查 `auto-aof-rewrite-min-size` 和日志。
+
+### 常用运维命令
+
+```bash
+# ---- 手动触发 AOF 重写 ----
+BGREWRITEAOF
+
+# ---- 查看 AOF 文件大小 ----
+ls -lh /var/lib/redis/appendonlydir/
+# Redis 7: 查看各组件文件
+ls -lh /var/lib/redis/appendonlydir/base.rdb
+ls -lh /var/lib/redis/appendonlydir/incr-*.aof
+
+# ---- 查看 AOF 文件内容(前 10 条命令,RESP 格式) ----
+head -20 /var/lib/redis/appendonlydir/incr-0000000000000003.aof
+
+# ---- 运行时动态调整 fsync 策略 ----
+redis-cli CONFIG SET appendfsync always # 临时切到最安全模式(如做批量导入前)
+redis-cli CONFIG SET appendfsync everysec # 恢复推荐配置
+```
+
+> [!tip] 运维实战技巧
+> **大批量数据导入时**:临时切到 `appendfsync no` 或 `appendfsync everysec`,导入完成后再切回 `always`(如果需要)。避免每条写命令都 fsync 导致导入耗时翻数十倍。
+>
+> **磁盘空间告急时**:手动触发 `BGREWRITEAOF` 可以立即压缩 AOF 文件体积,通常能减少 50%-80% 的空间占用。
+
## 关联笔记
- [[hhs/Redis/04-RDB持久化]] — RDB 快照机制
diff --git a/hhs/Redis/06-主从与哨兵.md b/hhs/Redis/06-主从与哨兵.md
index 4d32471..51d3d7b 100644
--- a/hhs/Redis/06-主从与哨兵.md
+++ b/hhs/Redis/06-主从与哨兵.md
@@ -230,7 +230,7 @@ flowchart LR
Replica 默认是 **只读** 的(`replica-read-only yes`),这不是 Redis 故意限制你,而是有充分的技术原因:
> [!WARNING] 为什么不应该在 Replica 上写数据?
-> 想象一下:你在 Replica 上写入了 `SET foo bar`,但 Master 上没有这个写入。下一次同步发生时,Master 会把自己的数据集"覆盖"到 Replica 上——你写的数据**悄无声息地消失了**。更糟糕的是,如果你在 Replica 上 `DEL` 一个不存在的 key,这个命令可能会被传播到 Master,导致 Master 上原本存在的数据被删掉。
+> 想象一下:你在 Replica 上写入了 `SET foo bar`,但 Master 上没有这个写入。下一次同步发生时,Master 会把自己的数据集"覆盖"到 Replica 上——你写的数据**悄无声息地消失了**。注意 Redis 复制是**单向的**(Master → Replica),你在 Replica 上的写入不会反向传播到 Master,但这本身就意味着 Replica 和 Master 之间的数据已经不一致了。
>
> **结论**:在 Replica 上写数据,本质上是在制造**数据不一致的定时炸弹**。
@@ -254,8 +254,9 @@ replica-lazy-flush no # FULLRESYNC 时清空旧数据的策略
# --- 副本同步相关 ---
repl-diskless-sync no # 无盘复制:Master 不写 RDB 文件,
- # 直接通过 socket 发给 Replica(适合 SSD 环境)
-repl-diskless-sync-delay 5 # 开启无盘复制后,等几秒让多个 Replica 同时接收
+ # 直接通过 socket 发给 Replica(适合磁盘 I/O 慢的环境,如云盘/HDD)
+repl-diskless-sync-delay 5 # 开启无盘复制后,等几秒让多个 Replica 同时连接并接收 RDB 流,
+ # 减少 Master 重复生成 RDB 的次数(不是随机延迟,有明确优化意图)
repl-timeout 60 # 同步超时阈值(秒),超时则断开重连
```
@@ -416,6 +417,9 @@ flowchart TD
| 2 | **`replica-priority` 最小** | 配置中人为指定的优先级 | priority=10 优先于 priority=100;priority=0 表示"永不选我" |
| 3(最低) | **Run ID 字典序最小** | 纯粹的 tie-breaker | 两个 Replica 前两项完全相同,选 Run ID 字母序靠前的 |
+> [!WARNING] 如果所有 Replica 都不健康怎么办?
+> Sentinel **不会强行执行故障转移**。如果所有 Replica 都已断线、数据严重滞后或不可达,Sentinel 宁可让系统保持不可用状态,也不会选出一个数据严重落后的 Replica 当 Master——这体现了"宁缺毋滥"的设计哲学。此时运维需要介入,排查 Replica 为什么全部掉线。
+
**Step 2:提升为新 Master**
Sentinel 向选中的 Replica 发送 `SLAVEOF NO ONE`,使其断开与旧 Master 的复制关系,变成独立的 Master。
diff --git a/hhs/Redis/07-集群方案.md b/hhs/Redis/07-集群方案.md
index 67a1ed5..374717b 100644
--- a/hhs/Redis/07-集群方案.md
+++ b/hhs/Redis/07-集群方案.md
@@ -75,7 +75,7 @@ rdb.Set(ctx, "foo", "bar", 0) // 自动路由到对应节点
### Gossip 协议运作方式
-每个节点每秒向其他节点发送 PING/PONG,维持集群状态的一致性:
+每个节点每秒向其他节点发送 PING/PONG,维持集群状态的一致性。选择的节点数量动态调整——集群越小越接近全发,集群越大越倾向于随机子集:
```mermaid
sequenceDiagram
@@ -87,7 +87,7 @@ sequenceDiagram
B->>A: PONG (含自身 + 选中节点的状态)
end
- Note over A: Gossip 收敛原理:
每次随机选 3 个节点交换信息,
N 个节点 ~ log₃(N) 轮即可全同步
+ Note over A: Gossip 收敛原理:
集群越大 fanout 越大,约 O(sqrt(N)),
O(log(N)) 轮即可全同步
```
**Gossip 传播的三类信息:**
@@ -96,7 +96,7 @@ sequenceDiagram
3. **FAIL** — 某节点不可达,广播给其他节点
> [!QUESTION] 思考
-> 为什么 Gossip 不直接发给所有节点,而是随机选 3 个?当集群扩展到 1000 台节点时,这样做的好处是什么?
+> Gossip 节点选择策略会随集群规模动态调整——小集群(<100 节点)向所有节点发 PING,大集群才随机选择子集。为什么不直接广播给所有节点?这样做的带宽开销是怎样的?
### PING/PONG 报文详解
@@ -120,9 +120,9 @@ CLUSTER SETSLOT MIGRATING
# 在目标节点执行(确认阶段)
CLUSTER SETSLOT IMPORTING
-# 逐 key 迁移(客户端负责搬 data)
-for key in $(redis-cli -h source-cluster-scan $slot --count 100); do
- redis-cli -h source MIGRATE target "" $key 0 5000
+# 逐 key 迁移(GETKEYSINSLOT 返回指定 slot 中的 key,每次最多 100 个)
+for key in $(redis-cli -h $SRC -p $SRC_PORT CLUSTER GETKEYSINSLOT $slot 100); do
+ redis-cli -h $SRC -p $SRC_PORT MIGRATE $DST $DST_PORT $key 0 5000
done
# 完成迁移
@@ -202,7 +202,8 @@ val, _ := rdb.Get(ctx, "key").Result()
|------|------|---------|
| 不支持多数据库 | 固定只有 db 0 | 应用层模拟 db(key 前缀) |
| 命令子集受限 | `KEYS`, `MIGRATE`, `SORT` 等不可用 | 用 `SCAN` 替代 `KEYS` |
-| Lua 脚本有限 | 不支持非确定性的函数(time/random) | 使用 `EVALSHA` 减少网络传输 |
+| 跨节点多 key 命令受限 | `DEL`, `MGET`, `MSET`, `RENAME` 等多 key 操作要求 key 同 slot | 用 hash tag 或应用层拆分为多次单 key 调用 |
+| Lua 脚本有限 | 所有 key **必须映射到同一 slot**;不支持非确定性函数(time/random) | 同 Lua 内 key 用同一 hash tag;用 `EVALSHA` 减少网络传输 |
| Pipeline 受限 | 同一 pipeline 中所有 key 必须在同一节点 | 用 hash tag `{user:100}:x` 对齐 |
| MULTI/EXEC 受限 | 同上,跨 key 事务不支持 | 用 Lua 脚本实现原子操作 |
@@ -376,6 +377,13 @@ let val = await cluster.get("key")
| `maxmemory-policy` | `allkeys-lru` | 集群场景统一设置 LRU 淘汰,避免部分节点 OOM |
| `appendonly` | `yes` | 集群推荐 AOF + 每秒刷盘,兼顾性能与安全 |
| `cluster-replica-validity-factor` | 10 | 乘以 timeout 作为复制链路容忍阈值,0 表示不拒绝主从链接 |
+| `cluster-require-full-coverage` | `yes`(默认) | `yes` 时任意 slot 无主节点则集群整体拒绝写入;生产环境若追求可用性可设 `no`,允许部分 slot 不可用时其余 slot 正常服务 |
+| `cluster-allow-reads-when-down` | `no` | 设为 `yes` 时集群即使处于 FAIL 状态也允许读请求(前提:该 key 所在 slot 有可达节点),适合读密集型业务容忍短暂降级 |
+
+> [!IMPORTANT] `cluster-require-full-coverage` 的取舍
+> 当某个 Master 和它的所有 Replica 同时宕机时,该 Master 负责的 slot 就"裸奔"了。
+> - **`yes`(默认)**:集群整体拒绝读写,宁可停机也不允许数据不一致——适合对一致性要求极高的金融场景。
+> - **`no`**:集群只拒绝访问故障 slot 的请求,其他 slot 继续服务——适合追求可用性的互联网业务,牺牲部分可用 slot 换取整体不宕机。
### 架构最佳实践
diff --git a/hhs/Redis/08-SortedSet精解.md b/hhs/Redis/08-SortedSet精解.md
index 077bf4f..919996a 100644
--- a/hhs/Redis/08-SortedSet精解.md
+++ b/hhs/Redis/08-SortedSet精解.md
@@ -7,7 +7,7 @@ create time: 2026-05-15 18:14
## 概述
-Sorted Set(有序集合)是 Redis 中**功能最丰富、场景最广泛**的数据结构。它的底层由 skiplist + hashtable 组成,天然支持按分数排序和 O(1) 的成员查找。本节聚焦实战中常用的高级模式。
+Sorted Set(有序集合)是 Redis 中**功能最丰富、场景最广泛**的数据结构。底层采用双编码策略:小集合(默认 ≤128 个元素,Redis 7.0+ 使用 listpack 替代 ziplist)用连续内存紧凑存储;超过阈值后自动切换为 **skiplist + hashtable** 组合,天然支持按分数排序和 O(log N) 的范围查询。本节聚焦实战中常用的高级模式。
## 排行榜系统
@@ -30,6 +30,10 @@ ZREVRANK leaderboard "user:101" # 2
ZRANGEBYSCORE leaderboard 3000 4000
```
+> [!WARNING] ZRANGEBYSCORE 已废弃(Redis 6.2+)
+> `ZRANGEBYSCORE` 在 Redis 6.2 起被统一到 `ZRANGE ... BYSCORE` 语法。本文档保留旧命令便于理解,但新代码建议写成:
+> `ZRANGE leaderboard 3000 4000 BYSCORE WITHSCORES`
+
### Go 代码示例
```go
@@ -112,6 +116,9 @@ ZREM delayed_tasks
LPUSH processing_queue
```
+> [!CAUTION] 多步操作需保证原子性
+> 上面的"查询 → 删除 → 转存"三步并非原子操作,多个消费者同时拉取会导致**重复消费**。生产环境应将这段逻辑封装到 Lua 脚本中,通过 `EVAL` 一次执行,或使用 `BZPOPMIN` 的阻塞弹出模型天然规避此问题。
+
> [!WARNING] 延迟队列不是消息队列
> 对于大量并发任务,ZSet 的 O(log N) 插入和维护成本会累积。考虑专用 MQ(Kafka/RabbitMQ)更合适。Redis ZSet 适合**规模适中(万级以内)**的延迟任务场景。
@@ -121,9 +128,9 @@ ZSet 天然适合做**固定时间窗口内的去重计数**(如每分钟独
```bash
# key = page:home:uv,score = unix timestamp ms,member = visitor_id
-ZREMRANGEBYSCORE page:home:uv $now_ms 60000 # 清除 60s 前的数据
+ZREMRANGEBYSCORE page:home:uv 0 $((now_ms - 60000)) # 清除 60s 窗口外的旧数据
ZADD page:home:uv $now_ms visitor:a visitor:b visitor:c
-SCARD page:home:uv # 当前窗口内独立访客数
+ZCARD page:home:uv # 当前窗口内独立访客数(ZSet 用 ZCARD,不是 SCARD)
```
```go
@@ -148,9 +155,10 @@ count, _ := rdb.ZCard(ctx, "page:home:uv").Result()
```bash
# === 固定窗口限流 ===
-# key = limit:user:1001, score = unix timestamp(每秒一个请求)
-ZREMRANGEBYSCORE limit:user:1001 $now_ts $(($now_ts - 1)) # 清除 1s 前数据
-ZADD limit:user:1001 $now_ts req:$RANDOM # 记录本次
+# 按整秒截断:窗口起点 = floor(now_s / 1) * 1
+WINDOW_START=$((now_ts - now_ts % 1)) # 当前秒的起始时间戳
+ZREMRANGEBYSCORE limit:user:1001 0 $((WINDOW_START - 1)) # 清除上一个窗口之前的数据
+ZADD limit:user:1001 $now_ts req:$RANDOM # 记录本次请求
ZCARD limit:user:1001 # 当前窗口 QPS
# === 滑动窗口限流(更精准)===
@@ -206,16 +214,23 @@ ZADD rank:us 800 "vip_user:A" 900 "user:C"
# 合并求和(默认分数相加)
ZUNIONSTORE global:top 2 rank:cn rank:us AGGREGATE SUM
+# AGGREGATE 三种模式:
+# SUM — 分数求和(默认,适合多维度累加)
+# MAX — 取各集合中的最大分数(适合"最高分"场景)
+# MIN — 取各集合中的最小分数(适合"保底分"场景)
# 取全局 Top 10
-ZRANGE global:top 0 9 WITHSCORES REVERSE
+ZREVRANGE global:top 0 9 WITHSCORES
# → vip_user:A(1800), user:C(900), user:B(500)...
```
```go
keys := []string{"rank:cn", "rank:us", "rank:jp"}
-// 合并三个区域排行榜,取分数最高的 20 名
-rdb.ZUnionStore(ctx, "global:top20", keys)
+// 第一个参数是目标 key,结果写入 "global:top"
+rdb.ZUnionStore(ctx, "global:top", &redis.ZStore{
+ Keys: keys,
+ Aggregate: "SUM", // 可选: "SUM" / "MAX" / "MIN"
+})
top20, _ := rdb.ZRevRangeWithScores(ctx, "global:top20", 0, 19).Result()
```
@@ -232,26 +247,30 @@ GEOADD cities 116.4074 "beijing" 121.4737 "shanghai" 108.9690 "nanning"
# 计算两点距离(米)
GEOPOS cities beijing shanghai # 获取经纬度坐标
-GEODIST cities beijing nanning km # 距离(km/m/fi 单位)
+GEODIST cities beijing nanning km # 距离(km/m/ft/mi 单位)
-# 附近的人(半径 50km 内)
-GEORADIUS cities 116.4074 39.9042 50 km WITHDIST WITHCOORD
+# 附近的人(半径 50km 内)—— Redis 6.2+ 推荐
+GEOSEARCH cities FROMLONLAT 116.4074 39.9042 BYRADIUS 50 km WITHDIST WITHCOORD ASC COUNT 5
+# ASC: 从近到远(默认)
-# 新版本 API(推荐)
-GEORADIUSBYMEMBER cities shanghai 100 km DESC COUNT 5
-# DESC: 从远到近(默认从近到远)
+# 以某个成员为中心搜索
+GEOSEARCH cities FROMMEMBER shanghai BYRADIUS 100 km DESC COUNT 5
+# DESC: 从远到近
```
+> [!WARNING] GEORADIUS / GEORADIUSBYMEMBER 已废弃
+> 这两个命令在 Redis 6.2 后被 `GEOSEARCH`(查询)和 `GEOSEARCHSTORE`(查询并存储)取代。旧命令仍可使用,但新代码应直接用 `GEOSEARCH`。
+
### 常见场景
| 场景 | 命令 | 注意 |
|------|------|------|
-| 附近门店 | GEORADIUSBYMEMBER | 限流 + 分页 |
-| 打车接单范围 | GEOADD + GEORADIUS | 结合 spatial index |
-| 围栏检测 | GEOPOS + 数学计算 | 超出范围触发告警 |
+| 附近门店 | `GEOSEARCH FROMMEMBER` | 限流 + `COUNT` 分页 |
+| 打车接单范围 | `GEOSEARCH FROMLONLAT` | 结合空间索引,`ASC` 按距离排序 |
+| 围栏检测 | `GEOPOS` + Haversine 公式 | 超出范围触发告警 |
> [!TIP] Geo 底层就是 Sorted Set
-> `GEOPOS` 本质是 `ZSCORE`(存的是 encode 后的 lat+lng),所以也可以用 ZREVRANGE 做通用查询。Geo 只是一层语法糖。
+> `GEOPOS` 本质是 `ZSCORE` + GeoHash 解码(score 存的是 52 位 GeoHash 编码值,不是原始经纬度),所以也可以用 `ZSCORE` 读出编码值,或用 `ZREVRANGE` 做通用范围查询。Geo 只是一层语法糖。
## 社交关系 —— 共同好友 / 标签匹配
@@ -282,12 +301,12 @@ ZADD rec:user:2 "go" 0.95 "python" 1.0 "flask" 0.6 "k8s" 0.9
ZUNIONSTORE temp:rec 2 rec:user:1 rec:user:2 AGGREGATE SUM
# 分数越高 = 共同兴趣越多越强
-ZRANGEBYSCORE temp:rec 1.5 2.0 REVERSE WITHSCORES
+ZREVRANGEBYSCORE temp:rec 2.0 1.5 WITHSCORES
# → go(1.95), k8s(1.6)
```
> [!WARNING] 大集合运算警告
-> `SINTER` / `SUNION` / `ZUNIONSTORE` 的时间复杂度是 O(N × M),在大型集合上可能阻塞主线程。建议预计算 + 定时刷新,或在低峰期异步完成。
+> `SINTER` / `SUNION` / `ZUNIONSTORE` 的时间复杂度是 O(N × M)(N 为最小集合的元素数,M 为集合个数),在大型集合上可能阻塞主线程。建议预计算 + 定时刷新,或在低峰期异步完成。
## 优先级队列
@@ -337,12 +356,12 @@ mindmap
ZRANGEBYSCORE + min/max
避免 OFFSET 深翻页
编码降级
- <64元素 <64B→ziplist
- ≥128元素→skiplist+hashtable
+ 小集合→listpack 紧凑编码
+ 大集合→skiplist+hashtable
```
> [!TIP] 编码转换的触发条件
-> `maxziplist` entries(默认 128)和 `maxziplistvalue`(默认 64 bytes)控制着 ziplist ↔ skiplist 的转变。在数据量较大时建议调大 `zset-max-ziplist-entries` 以利用 ziplist 的内存紧凑性,但要权衡查询性能。
+> 配置参数 `zset-max-ziplist-entries`(默认 128)和 `zset-max-ziplist-value`(默认 64 bytes)控制着 listpack(Redis 7.0 前为 ziplist)↔ skiplist 的转变。小集合优先使用 listpack 紧凑编码以节省内存,超过阈值自动升级为 skiplist + hashtable。在数据量较大时可适当调大 entries 阈值以延迟升级,但需权衡 listpack 的 O(N) 插入性能。
## 关联笔记