缓存与多级缓冲
→ 返回 高并发
高并发下的缓存不仅是「加快读」,还包括写合并、读副本、热 key 治理、与 MQ 协同。通用三级缓存见 缓存架构;本文侧重极端 QPS 下的策略。
读路径:多级缓冲
请求
→ L1 Caffeine(进程内,μs)
→ L2 Redis(ms,跨实例)
→ L3 DB / 回源计算
| 数据 | L1 | L2 | 说明 |
|---|---|---|---|
| 好友列表 | ✅ | ✅ | 变更少,TTL 长 |
| 当日步数 | 可选 | ✅ | 写频高,L1 短 TTL |
| Top100 全服榜 | ✅ | ✅ | 极热,1~3s 刷新 |
| 用户资料 | ✅ | ✅ | 与榜分离 |
LoadingCache<String, FriendList> friends = Caffeine.newBuilder()
.maximumSize(50_000)
.expireAfterWrite(5, MINUTES)
.build(uid -> redis.get("friends:" + uid));失效:关系链变更时 Pub/Sub 广播 invalidate(uid),防多节点 L1 不一致。
写路径:缓存作为「写缓冲」
高并发写不要每次都落 DB:
| 模式 | 流程 |
|---|---|
| Write-Behind | 先写 Redis,异步批量刷 DB |
| 合并写 | 内存攒 N 条或 T 秒,一次 Pipeline |
| 计数型 | INCR / ZINCRBY,天然原子 |
排行榜步数:Redis 为真实时源,MySQL 为冷备份。
热 key 与 Local Cache
热 key 问题
单 key QPS 超过单 Redis 节点上限(约 10 万级,视命令而定):
| 手段 | 说明 |
|---|---|
| 本地缓存 | 全服 Top100 每实例缓存 1s |
| 随机分片 | key:{shard} 写分散,读聚合 |
| 读写副本 | 读从 Redis 副本(注意复制延迟) |
| 拆分 | 全服榜改地区榜 |
热 key 探测
redis-cli --hotkeys
# 或 Monitor + 分析,业务埋点 key QPS三大问题(高并发视角)
详见 Redis,高并发补充:
| 问题 | 高并发场景 |
|---|---|
| 穿透 | 无效 userId 批量探测;布隆 + 空值 |
| 击穿 | 0 点榜单 key 新建 + 洪峰;互斥加载或逻辑过期 |
| 雪崩 | 大量 key 同秒过期;TTL 抖动 + 多级降级 |
逻辑过期(排行榜常用):value 内嵌 expireAt,物理 key 不过期,后台线程异步重建。
Cache Aside 在高并发写的注意点
推荐:更新 DB 后删缓存(或延迟双删)
高并发写榜:Often 只写 Redis,DB 异步 —— 不适用经典双删,需对账
| 场景 | 策略 |
|---|---|
| 步数上报 | 只写 Redis + MQ,DB 滞后 |
| 资料修改 | 删 Redis + 广播 L1 失效 |
Pipeline 与 Lua
批量读好友步数:
redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
for (String fid : friendIds) {
conn.stringCommands().get(("step:20240627:" + fid).getBytes());
}
return null;
});合并写用 Lua 保证 compare-and-set 步数。
与缓冲层协作
上报 → MQ(缓冲)→ 消费者写 Redis(内存缓冲)
读榜 → L1 → Redis
T+1 → Redis 快照 → DB