vault backup: 2026-05-25 23:50:33
This commit is contained in:
+97
-21
@@ -74,10 +74,12 @@ func UpdateUser(ctx context.Context, u *User) error {
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING] Cache-Aside 的最终一致性
|
||||
> 写操作后立刻读,可能读到旧值(因为删缓存比下一个读请求先执行)。对于强一致场景,可以:
|
||||
> - 读操作加一把短锁(避免并发刷缓存覆盖新值)
|
||||
> - 或者直接用 DB 作为唯一真相源(放弃缓存一致性依赖)
|
||||
> [!WARNING] Cache-Aside 的竞态窗口
|
||||
> 典型竞态:读请求 A 先 miss 缓存并开始查 DB,此时写请求 B 更新 DB 并删缓存;A 从 DB 取到**旧值**后回填缓存,覆盖了 B 的删除操作。后续读请求将持续拿到旧数据。
|
||||
> 解决方案:
|
||||
> - **延迟双删**:写后删一次缓存,延迟几百毫秒再删一次(覆盖并发读的回填)
|
||||
> - **读操作加短锁**:回填缓存前加锁,避免并发读覆盖新值
|
||||
> - **版本号 / 时间戳**:回填时比对版本,旧版本不写入缓存
|
||||
|
||||
### Read/Write Through(读写穿透)
|
||||
|
||||
@@ -96,6 +98,26 @@ flowchart LR
|
||||
> [!NOTE] 实际中较少纯原生实现
|
||||
> Redisson(Java)、Spring Cache 提供此抽象;Go 生态没有标准库,通常自行封装。核心思路是 **用 AOP / Middleware 把缓存逻辑从业务代码中剥离**。
|
||||
|
||||
### Write-Behind(异步写回)—— 高写入吞吐场景
|
||||
|
||||
与 Read/Write Through 不同,Write-Behind 将写操作**先落到缓存**,再**异步批量刷回 DB**,适合写密集型场景(如计数器、日志采集)。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
App["写请求"] --> Cache["Redis<br/>立即返回 ✅"]
|
||||
Cache -->|"异步队列<br/>批量合并"| Writer["写回 Worker"]
|
||||
Writer --> DB["MySQL"]
|
||||
|
||||
style App fill:#efe,stroke:#090
|
||||
style Cache fill:#fda,stroke:#da0
|
||||
style Writer fill:#ddf,stroke:#66c
|
||||
```
|
||||
|
||||
> [!WARNING] Write-Behind 的风险
|
||||
> - **数据丢失风险**:缓存宕机时,尚未刷回 DB 的数据会丢失
|
||||
> - **实现复杂度高**:需要可靠的异步队列 + 重试机制 + 幂等保证
|
||||
> - **适用场景有限**:适合"可容忍少量丢失"的计数、埋点类业务,**不适合核心交易数据**
|
||||
|
||||
## 二、三大经典问题
|
||||
|
||||
### 缓存穿透(Penetration)—— 不存在的数据一直打到 DB
|
||||
@@ -192,32 +214,47 @@ sequenceDiagram
|
||||
|
||||
```go
|
||||
func GetProduct(ctx context.Context, id int64) (*Product, error) {
|
||||
data, _ := rdb.Get(ctx, "product:"+strconv.FormatInt(id, 10)).Bytes()
|
||||
cacheKey := "product:" + strconv.FormatInt(id, 10)
|
||||
|
||||
// 先查缓存
|
||||
data, _ := rdb.Get(ctx, cacheKey).Bytes()
|
||||
if data != nil {
|
||||
var p Product
|
||||
json.Unmarshal(data, &p)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
|
||||
// 拿分布式锁,只有一个 goroutine 去查 DB + 重建缓存
|
||||
lockKey := "lock:product:" + strconv.FormatInt(id, 10)
|
||||
locked, err := rdb.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
|
||||
if err != nil || !locked {
|
||||
time.Sleep(50 * time.Millisecond) // 等别人重建完
|
||||
return GetProduct(ctx, id) // 递归重试(有最大深度保护)
|
||||
lockKey := "lock:" + cacheKey
|
||||
const maxRetries = 3
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
locked, _ := rdb.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
|
||||
if locked {
|
||||
defer rdb.Del(ctx, lockKey)
|
||||
break
|
||||
}
|
||||
// 未拿到锁 → 等别人重建完后再查缓存
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if v, _ := rdb.Get(ctx, cacheKey).Bytes(); v != nil {
|
||||
var p Product
|
||||
json.Unmarshal(v, &p)
|
||||
return &p, nil
|
||||
}
|
||||
if i == maxRetries-1 {
|
||||
return nil, errors.New("获取锁超时,降级处理")
|
||||
}
|
||||
}
|
||||
defer rdb.Del(ctx, lockKey)
|
||||
|
||||
|
||||
// 双重检查——防止自己等太久,另一个已经重建了
|
||||
data, _ = rdb.Get(ctx, "product:"+strconv.FormatInt(id, 10)).Bytes()
|
||||
data, _ = rdb.Get(ctx, cacheKey).Bytes()
|
||||
if data != nil {
|
||||
var p Product
|
||||
json.Unmarshal(data, &p)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
|
||||
p, _ := db.GetProduct(id)
|
||||
rdb.Set(ctx, "product:"+strconv.FormatInt(id, 10), toJSON(p), 30*time.Minute)
|
||||
rdb.Set(ctx, cacheKey, toJSON(p), 30*time.Minute)
|
||||
return p, nil
|
||||
}
|
||||
```
|
||||
@@ -257,10 +294,22 @@ if time.Now().Unix() < item.ExpireAt {
|
||||
}
|
||||
|
||||
// 异步重建缓存(不阻塞当前请求)
|
||||
// ⚠️ 需配合 singleflight 或分布式锁,防止并发触发大量重建
|
||||
go rebuildCacheAsync(key, item.ExpireAt)
|
||||
return item.Content // 返回旧值,后台静默刷新
|
||||
```
|
||||
|
||||
#### 如何发现热点 Key?
|
||||
|
||||
击穿的前提是"热点",但如果不知道哪些 Key 是热点,防护就无从谈起:
|
||||
|
||||
| 方案 | 原理 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| **Redis `MONITOR` / `redis-cli --hotkeys`** | Redis 4.0+ 的 LFU 采样,直接输出高频 Key | 开发调试、小规模集群 |
|
||||
| **客户端代理统计** | 在 SDK 层埋点,按 Key 维度统计 QPS | 生产环境精确统计 |
|
||||
| **Redis Cluster `CLUSTER COUNTKEYSINSLOT`** | 统计 slot 级别的 Key 分布 | 定位数据倾斜 |
|
||||
| **离线分析慢查询日志** | `SLOWLOG GET` + 日志聚合 | 事后分析 |
|
||||
|
||||
### 缓存雪崩(Avalanche)—— 大量 key 同时过期
|
||||
|
||||
> [!QUESTION] 穿透/击穿/雪崩 的区别是什么?
|
||||
@@ -368,10 +417,37 @@ flowchart LR
|
||||
| 4 | 大 Key 拆小 | 单个 Key ≤ 5KB,防止网络阻塞 |
|
||||
| 5 | Pipeline 批量操作 | 减少 RTT,提升吞吐 |
|
||||
| 6 | 监控命中率 | 低于 80% 考虑调整策略 |
|
||||
| 7 | 不要缓存不可变数据 | 配置类可存本地内存 |
|
||||
| 8 | 敏感数据不入库 | 密码、手机号要脱敏或加密 |
|
||||
| 7 | 静态数据不必放 Redis | 配置、字典等低频变化数据用本地缓存即可,节省网络开销 |
|
||||
| 8 | 敏感数据不缓存明文 | 密码、手机号等脱敏或加密后再写入缓存 |
|
||||
|
||||
## 四、常见架构模式速览
|
||||
## 四、进阶:基于 Binlog 的缓存一致性
|
||||
|
||||
Cache-Aside 的"先删缓存再更新 DB"存在竞态窗口。生产中更可靠的方案是**监听 DB 变更日志(Binlog),异步驱动缓存失效**。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
App["业务写请求"] --> DB["MySQL"]
|
||||
DB -->|"Binlog"| Canal["Canal / Debezium"]
|
||||
Canal --> MQ["Kafka / RocketMQ"]
|
||||
MQ --> Consumer["缓存失效 Worker"]
|
||||
Consumer -->|"DEL key"| Redis["Redis"]
|
||||
|
||||
style DB fill:#ddf,stroke:#66c
|
||||
style Canal fill:#fda,stroke:#da0
|
||||
style Redis fill:#fcc,stroke:#c00
|
||||
```
|
||||
|
||||
> [!QUESTION] 这种方案解决了什么问题?
|
||||
> - **解除业务代码耦合**:写操作不需要关心缓存失效,由 Binlog 订阅统一处理
|
||||
> - **减少竞态窗口**:Binlog 保证有序,可以按变更顺序精确删除缓存
|
||||
> - **适合多数据源**:一次 DB 变更可以同时驱动多个缓存实例失效
|
||||
|
||||
> [!NOTE] 实际落地注意点
|
||||
> - **延迟**:Binlog → MQ → Consumer 链路通常有 **100ms~1s** 延迟,仍属于最终一致性
|
||||
> - **幂等**:Consumer 必须保证幂等(重复消费同一 Binlog 不产生副作用)
|
||||
> - **顺序性**:同一行的多次变更需要保证消费顺序,通常按 `ROW_ID` 分区
|
||||
|
||||
## 五、常见架构模式速览
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -395,7 +471,7 @@ flowchart TD
|
||||
style DB fill:#ddf,stroke:#66c
|
||||
```
|
||||
|
||||
## 五、缓存选型与演进路径
|
||||
## 六、缓存选型与演进路径
|
||||
|
||||
> [!QUESTION] 项目初期应该从哪一层开始做缓存?
|
||||
> **答案:先做 L2(Redis),稳定后再加 L1(本地缓存)。** 原因如下:
|
||||
@@ -433,7 +509,7 @@ flowchart TD
|
||||
| **V3 大规模** | 极致性能和稳定性 | 多级缓存 + 逻辑过期 + 降级策略 | 一致性 vs 可用性的平衡取舍 |
|
||||
|
||||
> [!TIP] 一句话总结
|
||||
> **好的缓存架构不是设计出来的,是演进出來的**。先保证正确性,再优化性能,最后打磨可用性。
|
||||
> **好的缓存架构不是设计出来的,是演进出来的**。先保证正确性,再优化性能,最后打磨可用性。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
|
||||
Reference in New Issue
Block a user