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) 插入性能。 ## 关联笔记