第一章:缓存体系全景与Redis核心角色
1.1 为什么缓存是高并发系统不可或缺的组件?
在现代分布式系统中,缓存已经不再是“可选优化”,而是系统性能、吞吐量、响应延迟的核心支柱。
- 提升性能:热点数据直接命中缓存,访问延迟从毫秒级降低至微秒级。
- 减轻数据库压力:避免频繁 IO,降低写入冲突。
- 应对突发高并发:缓存是系统抗压的第一道防线。
1.2 Redis在缓存中的核心优势
特性 | 说明 |
---|---|
极致性能 | 单线程模型,QPS 可达 10w+ |
数据结构丰富 | 支持 String、List、Set、Hash、ZSet |
天然持久化 | RDB/AOF 支持 |
支持高可用 | Sentinel、Cluster |
支持分布式锁 | SETNX、RedLock |
1.3 缓存问题的“病根”与分类
缓存虽好,但如果管理不当,常见以下三大类问题:
问题类型 | 触发条件 | 危害 |
---|---|---|
缓存穿透 | 请求的数据缓存与数据库都不存在 | 直接穿透数据库,大量查询压力 |
缓存击穿 | 热点 key 过期瞬间被并发请求击穿 | 大量并发直接打到数据库 |
缓存雪崩 | 大量 key 同时过期,或Redis集群不可用 | 瞬间所有请求打爆后端 |
1.4 缓存问题三件套图解
┌──────────────┐
│ Client │
└────┬─────────┘
▼
┌──────────────┐
│ Redis 缓存层 │
└────┬─────────┘
miss ▼
┌──────────────┐
│ MySQL/Postgre│
└──────────────┘
穿透:客户端请求非法ID,缓存和DB都miss
击穿:key刚失效,瞬间大量并发打到DB
雪崩:缓存层整体崩溃或大批量key同时失效
第二章:缓存穿透详解
2.1 概念定义
缓存穿透指的是客户端请求数据库和缓存中都不存在的key,由于缓存没有命中,每次请求都打到数据库,导致数据库压力激增。
2.2 穿透场景复现
示例:客户端请求不存在的用户 ID(如 -1
)
public User getUser(Long id) {
String cacheKey = "user:" + id;
User user = redis.get(cacheKey);
if (user != null) return user;
user = db.queryUser(id); // 如果 id 不存在,这里返回 null
if (user != null) {
redis.set(cacheKey, user, 3600);
}
return user;
}
如果大量恶意请求访问 user:-1
,此代码将不断访问数据库!
2.3 产生原因分析
- 用户请求非法 ID(如负数、随机 UUID)
- 缺乏参数校验
- 没有缓存空值
- 黑产刷接口绕过缓存层
2.4 穿透图解
Client ——> Redis ——miss——> DB ——return null
↑ ↓
↑——————————(没有缓存空值)——————————↑
2.5 缓存穿透解决方案
✅ 方法一:缓存空对象
if (user == null) {
redis.set(cacheKey, "", 300); // 缓存空值,短 TTL
}
缺点:容易污染缓存,适合低频查询接口。
✅ 方法二:布隆过滤器(推荐)
- 初始化阶段将所有合法ID添加至布隆过滤器
- 请求前先判断是否存在
// 初始化阶段
bloomFilter.put(10001L);
// 查询阶段
if (!bloomFilter.mightContain(id)) {
return null;
}
Redis 中可结合 RedisBloom 模块使用
✅ 方法三:参数合法性校验
if (id <= 0) return null;
第三章:缓存雪崩详解
3.1 什么是缓存雪崩?
指大量缓存 Key 同时失效,导致所有请求直接访问数据库,或 Redis 实例宕机后导致后端承压甚至宕机。
3.2 场景演示
// 设置缓存时使用固定TTL
redis.set("product:1", product, 3600);
redis.set("product:2", product, 3600);
redis.set("product:3", product, 3600);
当 3600s 后这些 key 全部过期,大量请求将同时穿透缓存。
3.3 雪崩图解
大量缓存key失效
▼
Redis层命中率骤降
▼
数据库 QPS 爆炸
▼
系统崩溃
3.4 缓存雪崩防护策略
✅ 随机过期时间
int ttl = 3600 + new Random().nextInt(600); // 1~10分钟偏移
redis.set(key, value, ttl);
✅ 多级缓存策略(本地缓存 + Redis)
- 一级缓存:Caffeine/Guava
- 二级缓存:Redis
- 第三级:数据库
// 查询顺序:local -> redis -> db
✅ 熔断/限流/降级
结合 Hystrix/Sentinel 对 Redis 异常进行降级兜底。
✅ 异步预热 + 主动刷新
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
refreshHotKeys();
}, 0, 10, TimeUnit.MINUTES);
第四章:缓存击穿深度解析与实战应对
4.1 什么是缓存击穿?
缓存击穿(Cache Breakdown) 指的是:
某一个热点 Key 在某一时刻突然过期,大量请求并发访问这个 Key 时,发现缓存已过期,全部落到数据库上查询,导致系统瞬间压力飙升,甚至出现“雪崩式击穿”。
4.2 击穿 vs 雪崩 vs 穿透区别
问题类型 | 对象 | 成因 | 危害 |
---|---|---|---|
穿透 | 不存在的 Key | 缓存和 DB 都查不到 | 每次都访问数据库 |
雪崩 | 大量 Key | 大批缓存同时过期 / Redis崩溃 | 大量请求直达数据库 |
击穿 | 热点单 Key | 缓存恰好过期,瞬时高并发访问 | 大量请求集中落入数据库压力爆表 |
4.3 场景还原(代码示例)
假设一个热点商品详情页(例如活动页 banner ID 为 10001):
public Banner getBanner(long id) {
String cacheKey = "banner:" + id;
Banner banner = redis.get(cacheKey);
if (banner != null) return banner;
banner = db.queryBanner(id);
if (banner != null) {
redis.set(cacheKey, banner, 60); // 设置 60s 缓存
}
return banner;
}
如果这段时间正好是 高并发秒杀活动开始前 5 秒,正值用户大量涌入访问页面,缓存 TTL 恰巧过期 —— 所有请求直接穿透落入 DB,引发 缓存击穿。
4.4 图解缓存击穿
缓存中 key = “banner:10001” 正好过期
▼
多个用户同时请求此 key
▼
Redis 全部 miss,直接穿透
▼
所有请求查询 DB,系统资源暴涨
Client1 ─┐
Client2 ─┴──► Redis (Miss) ─► DB
Client3 ─┘
4.5 缓存击穿常见场景
场景 | 描述 |
---|---|
秒杀商品详情页 | 商品信息查询量极高,缓存失效后容易并发打 DB |
热门推荐数据 | 类似“今日热榜”、“最新视频”等,属于短时高热缓存数据 |
实时数据缓存 | 缓存设为短 TTL,需要高频更新 |
用户登录态(短期有效) | Session 失效时并发访问,易触发击穿 |
4.6 击穿防护策略
✅ 方案一:互斥锁(推荐)
对某个 key 的缓存构建操作加锁,防止并发构建重复查询数据库。
String lockKey = "lock:banner:" + id;
boolean locked = redis.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (locked) {
try {
Banner banner = db.queryBanner(id);
redis.set("banner:" + id, banner, 60);
} finally {
redis.delete(lockKey);
}
} else {
Thread.sleep(50); // 等待其他线程构建缓存后重试
return getBanner(id); // 递归重试
}
Redis SETNX
是加锁核心,避免多线程同时构建缓存。
✅ 方案二:逻辑过期 + 异步刷新(热点数据适用)
逻辑上设置过期时间,但物理上仍保留旧值。由后台线程定期刷新热点缓存。
{
"data": {...},
"expireTime": "2025-07-03T15:00:00Z"
}
- 客户端每次读取
expireTime
,若当前时间超出则触发异步更新线程刷新缓存。
if (now().isAfter(data.expireTime)) {
// 异步刷新缓存数据,当前线程继续使用旧值
}
✅ 方案三:缓存永不过期 + 定时刷新
// 设置为永久 TTL
redis.set("banner:10001", data);
// 每隔 X 分钟由调度线程刷新缓存
@Scheduled(cron = "0 */5 * * * ?")
public void refreshHotBanner() {
Banner banner = db.queryBanner(10001);
redis.set("banner:10001", banner);
}
✅ 方案四:本地缓存兜底
- 使用 Guava / Caffeine 实现本地 LRU 缓存机制
- Redis 失效时快速兜底(适合小容量热点数据)
LoadingCache<String, Banner> localCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build(key -> db.queryBanner(Long.parseLong(key)));
4.7 防护策略对比分析表
方案 | 原理 | 适用场景 | 缺点 |
---|---|---|---|
互斥锁 | SETNX 防止并发 | 中低并发场景 | 存在短暂等待 |
逻辑过期 + 异步刷新 | 数据中标记过期时间 | 高并发热点 key | 数据可能短暂过期 |
永不过期 + 定时刷新 | 定时主动更新 | 一致性要求低 | 数据延迟大 |
本地缓存兜底 | JVM 内存快速命中 | 热点数据小 | JVM 重启或更新需同步策略 |
4.8 实战案例:用户信息缓存击穿防护
public User getUserById(Long userId) {
String key = "user:" + userId;
String lockKey = "lock:" + key;
String cached = redis.get(key);
if (cached != null) return deserialize(cached);
if (redis.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS)) {
try {
User user = db.queryUser(userId);
redis.set(key, serialize(user), 3600);
return user;
} finally {
redis.delete(lockKey);
}
} else {
try {
Thread.sleep(50);
} catch (InterruptedException e) {}
return getUserById(userId); // 重试
}
}
4.9 面试典型问题讲解
Q:如何解决 Redis 缓存击穿问题?
答: 常用方式是为热点 Key 加互斥锁防止缓存重建并发访问;或使用逻辑过期 + 异步刷新方案实现数据容忍性;高并发场景建议组合多级缓存策略防止单点故障。
第五章:多级缓存架构与数据一致性机制实战
这一章节将深入剖析缓存系统在真实业务中如何设计为多级缓存架构(L1+L2+DB),并重点解决实际开发中常见的缓存一致性、更新延迟、双写失效等问题。
5.1 为什么需要多级缓存架构?
5.1.1 单级缓存的局限性
如果仅使用 Redis:
- 网络访问成本仍然高于本地访问
- 遇到 Redis 宕机或波动,缓存整体不可用
- 缓存刷新时会出现抖动或击穿
5.1.2 多级缓存的优势
缓存级别 | 描述 | 优点 |
---|---|---|
一级缓存 | 本地缓存(如 Caffeine) | 访问快,读写成本低 |
二级缓存 | Redis 分布式缓存 | 容量大、集群支撑能力强 |
第三级 | 后端数据库 | 最终数据源,写一致性保障 |
5.1.3 多级缓存系统架构图
┌────────────────────────────┐
│ Application │
└────────────┬───────────────┘
▼
┌───────────────────┐
│ L1: 本地缓存 │ ← Caffeine/Guava (TTL短)
└────────┬──────────┘
▼
┌───────────────────┐
│ L2: Redis缓存层 │ ← 分布式缓存 (TTL长)
└────────┬──────────┘
▼
┌───────────────────┐
│ DB持久层 │ ← MySQL / PostgreSQL
└───────────────────┘
5.2 多级缓存代码实践(Java)
5.2.1 使用 Caffeine + Redis 的组合模式
LoadingCache<String, User> localCache = Caffeine.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.maximumSize(1000)
.build(key -> {
// 若本地未命中,则查询 Redis
String json = redis.get(key);
if (json != null) {
return JSON.parseObject(json, User.class);
}
// 再次未命中,则查询数据库
User user = db.queryUser(Long.parseLong(key.split(":")[1]));
redis.set(key, JSON.toJSONString(user), 10 * 60); // 10分钟缓存
return user;
});
✅ 说明:
- 本地缓存:2分钟,适合短期热点命中
- Redis 缓存:10分钟,作为统一缓存层支撑大量请求
- DB:作为最终数据源,仅在两层缓存都失效后访问
5.3 缓存一致性问题与挑战
5.3.1 常见问题场景
场景一:更新数据库后忘记更新缓存
user.setAge(30);
db.update(user);
// ❌ 忘记更新 Redis
场景二:先更新缓存,再更新数据库,结果失败
redis.set("user:123", user);
db.update(user); // 此处失败,缓存已脏
5.4 缓存一致性更新策略
✅ 5.4.1 推荐策略一:更新数据库 → 删除缓存
db.update(user); // ✅ 先更新数据库
redis.delete("user:" + user.getId()); // ✅ 后删除缓存
延迟一段时间后用户访问缓存 miss,重新从数据库加载
✅ 5.4.2 延迟双删机制(高并发安全型)
db.update(user); // 第一次删除缓存
redis.delete("user:" + user.getId());
Thread.sleep(500); // 短暂等待(让并发请求构建缓存)
redis.delete("user:" + user.getId()); // 第二次删除兜底
优点:防止并发请求在第一次删除后又提前构建新缓存,第二次删除保证脏数据清理。
✅ 5.4.3 读写分离设计:写请求不使用缓存
// 读:从缓存查找用户
public User getUser(id) {
// 优先使用 Caffeine -> Redis -> DB
}
// 写:只更新数据库 + 删除缓存,不写入缓存
public void updateUser(User user) {
db.update(user);
redis.delete("user:" + user.getId());
localCache.invalidate("user:" + user.getId());
}
5.5 高并发场景下的数据一致性问题详解
5.5.1 问题:读写并发 + 延迟写成功 → 缓存脏数据
- 请求A:删除缓存 → 更新数据库(慢)
- 请求B:并发访问,发现缓存为空,访问数据库旧数据 → 重建缓存(错误)
- 请求A 继续 → 数据库更新完成,但缓存被错误重建
5.5.2 解决方案:逻辑过期 / 异步延迟删除 / 分布式锁保护
5.6 分布式缓存一致性实战:Redis Keyspace Notification + 消息队列
5.6.1 Redis Key 事件通知(keyspace)
开启配置:
notify-keyspace-events Egx
监听 key 过期:
PSUBSCRIBE __keyevent@0__:expired
可用于触发缓存刷新:
// key 过期事件订阅后,重新构建缓存
5.7 多级缓存一致性问题总结表
问题 | 场景描述 | 防御方案 |
---|---|---|
并发重建脏缓存 | 缓存刚被删除,缓存构建先于 DB 更新 | 延迟双删 |
脏数据缓存失败 | 先写缓存,后写 DB,DB 写失败 | 先更新 DB,再删缓存 |
缓存更新被覆盖 | DB 改完后,旧请求更新了缓存 | 分布式锁 / 写队列控制并发写入 |
跨服务缓存不一致 | 服务 A 删缓存,B 未感知 | Redis Key 事件 + MQ 同步 |
5.8 SpringBoot + Caffeine + Redis 多级缓存实战架构
Spring 配置:
spring:
cache:
type: caffeine
caffeine:
spec: expireAfterWrite=120s,maximumSize=1000
RedisConfig 注册缓存:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.maximumSize(1000));
return manager;
}
}
5.9 多级缓存适用场景建议
场景 | 推荐策略 |
---|---|
高并发热点数据 | Caffeine + Redis + 异步刷新 |
用户 Session/权限数据缓存 | Redis + TTL 逻辑刷新机制 |
长时间不变的数据(配置类) | Redis 永不过期 + 定时刷新 |
实时变动数据(行情、库存) | Redis + MQ 异步通知刷新 |
第六章:Redis 缓存监控、容灾与故障恢复策略实战
6.1 缓存监控指标体系
6.1.1 关键指标概览(Prometheus 采集项)
监控项 | 含义 |
---|---|
redis_connected_clients | 当前客户端连接数量 |
redis_memory_used_bytes | Redis 占用内存大小 |
redis_keyspace_hits | 缓存命中次数 |
redis_keyspace_misses | 缓存未命中次数 |
redis_evicted_keys | 被淘汰的 key 数 |
redis_expired_keys | 过期删除的 key 数 |
redis_commands_processed_total | Redis 执行命令总数 |
redis_instance_uptime | 实例运行时长 |
redis_latency | 命令响应延迟 |
6.1.2 构建「命中率图表」
公式:
命中率 = hits / (hits + misses)
命中率持续下降 → 可能是雪崩或击穿前兆!
6.1.3 构建「内存预警系统」
内存耗尽往往意味着 Redis 会开始逐出 key 或拒绝写入,需结合以下配置和指标:
- 配置项
maxmemory
- 策略项
maxmemory-policy
(推荐使用:volatile-lru / allkeys-lru)
6.2 使用 Redis Exporter + Prometheus + Grafana 实现可视化
6.2.1 安装 Redis Exporter
docker run -d -p 9121:9121 oliver006/redis_exporter
6.2.2 Prometheus 配置示例
scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['localhost:9121']
6.2.3 Grafana 仪表盘示例(热门 Dashboard ID)
- 官方推荐 Dashboard ID:763(Redis)
- 支持 Key 命中率、QPS、连接数、内存曲线等
6.3 雪崩与击穿的早期告警机制
风险行为 | 异常指标变化 | 告警方式 |
---|---|---|
雪崩开始 | 命中率下降、miss率上升 | 报警阈值设置 + 邮件/钉钉 |
击穿发生 | 某 key 请求数异常增长 | 热 key 检测 |
内存逼近限制 | memory\_used\_bytes 接近 maxmemory | 自动扩容 + 限流 |
节点掉线 | 节点无响应 | Sentinel 哨兵自动切换 |
6.4 Sentinel 容灾与主从故障切换
6.4.1 Sentinel 简介
Redis Sentinel 是官方提供的高可用监控工具,支持:
- 主节点宕机时自动切换
- 向客户端通知新主节点地址
- 哨兵节点之间投票选举 Leader
6.4.2 Sentinel 架构图
┌─────────────┐
│ 客户端 │
└─────┬───────┘
▼
┌─────────────┐
│ Sentinel 集群│
└────┬────────┘
▼
┌────────────┐
│ 主节点 Master│
└────┬────────┘
▼
┌────────────┐
│ 从节点 Slave│
└────────────┘
6.4.3 配置 Sentinel 示例(sentinel.conf)
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
6.4.4 客户端连接示例(Jedis)
Set<String> sentinels = new HashSet<>();
sentinels.add("localhost:26379");
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels);
try (Jedis jedis = pool.getResource()) {
jedis.set("key", "value");
}
Redis Sentinel 自动发现主从变更,无需重启客户端。
6.5 Redis Cluster 容灾架构(支持自动分片)
适用于大规模部署,自动分片、高可用容灾:
+---------+ +---------+ +---------+
| Master1 |<-> | Master2 |<-> | Master3 |
| Slot 0-5000 | 5001-10000 | 10001-16383 |
| | | | | |
| Slave1 | | Slave2 | | Slave3 |
+---------+ +---------+ +---------+
- 每个 Master 控制部分 Slot
- 每个 Master 都有 Slave 自动备份
- 故障节点由其他 Master 代理投票恢复
6.6 容灾方案对比
方案 | 特点 | 自动切换 | 写入可用性 | 成本 |
---|---|---|---|---|
Redis Sentinel | 哨兵+主从+投票机制 | ✅ | 单主写入 | 中 |
Redis Cluster | 自动分片+多主架构 | ✅ | 可配置多写 | 高 |
手动主备 | 脚本控制主从切换 | ❌ | 写需切换 DNS | 低 |
6.7 故障模拟演练与自恢复测试
6.7.1 主动 kill 掉 Redis 主节点
docker exec -it redis_master bash
kill 1
观察:
- Sentinel 是否能在 5s 内识别故障?
- 客户端是否自动连接新主?
- 缓存数据是否同步成功?
6.8 限流与降级机制补充缓存防线
6.8.1 热点 Key 限流
使用滑动窗口算法,限制单位时间内某 key 访问次数
if (redis.incr("req:user:1001") > 100) {
return "Too many requests";
}
redis.expire("req:user:1001", 60);
6.8.2 服务降级保护缓存层
结合 Sentinel / Hystrix / Resilience4j,熔断缓存访问失败后自动降级到兜底响应或返回缓存快照。
第七章:高并发下的缓存穿透、雪崩、击穿综合实战项目
7.1 项目目标架构图
┌──────────────┐
│ Client │
└─────┬────────┘
▼
┌────────────────────┐
│ Controller层 │
└────────┬───────────┘
▼
┌───────────────────────────────────────┐
│ CacheService(缓存综合服务) │
│ ┌────────┐ ┌──────────────┐ │
│ │布隆过滤│ │ 本地缓存 Caffeine│ │
│ └────────┘ └──────────────┘ │
│ ┌────────────┐ │
│ │Redis 二级缓存│ ←→ 分布式锁 │
│ └────────────┘ │
│ ┌────────────┐ │
│ │ DB 层 │ ←→ MQ缓存刷新通知 │
│ └────────────┘ │
└───────────────────────────────────────┘
7.2 实战项目技术栈
模块 | 技术 |
---|---|
Spring 框架 | Spring Boot 3.x |
缓存组件 | Caffeine、Redis |
锁组件 | Redis 分布式锁 |
过滤组件 | RedisBloom |
限流组件 | Guava RateLimiter |
消息中间件 | RabbitMQ / Kafka |
日志 & 监控 | SLF4J + Micrometer |
7.3 本地缓存 Caffeine 配置
@Bean
public Cache<String, Object> localCache() {
return Caffeine.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
}
7.4 Redis 二级缓存访问逻辑(Cache Aside)
public User queryUserById(Long userId) {
String key = "user:" + userId;
// 1. 先查本地缓存
User user = (User) localCache.getIfPresent(key);
if (user != null) return user;
// 2. 查 Redis 缓存
String redisVal = redisTemplate.opsForValue().get(key);
if (StringUtils.hasText(redisVal)) {
user = JSON.parseObject(redisVal, User.class);
localCache.put(key, user); // 回填本地缓存
return user;
}
// 3. 缓存穿透防护:布隆过滤器判断是否存在
if (!bloomFilter.contains(key)) {
return null;
}
// 4. 加锁防击穿
String lockKey = "lock:" + key;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (!locked) {
// 等待其他线程构建缓存
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
return queryUserById(userId); // 重试
}
try {
// 5. 查询数据库
user = userMapper.selectById(userId);
if (user == null) {
redisTemplate.opsForValue().set(key, "", 120, TimeUnit.SECONDS); // 空值防穿透
} else {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 10, TimeUnit.MINUTES);
localCache.put(key, user);
}
return user;
} finally {
redisTemplate.delete(lockKey); // 解锁
}
}
7.5 更新缓存:延迟双删机制实现
@Transactional
public void updateUser(User user) {
String key = "user:" + user.getId();
// 1. 先更新数据库
userMapper.updateById(user);
// 2. 删除缓存
redisTemplate.delete(key);
localCache.invalidate(key);
// 3. 延迟二次删除
Executors.newSingleThreadScheduledExecutor()
.schedule(() -> {
redisTemplate.delete(key);
localCache.invalidate(key);
}, 500, TimeUnit.MILLISECONDS);
}
7.6 热点 Key 检测与限流
RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100个请求
public Object queryHotData(String key) {
if (!rateLimiter.tryAcquire()) {
log.warn("限流触发:{}", key);
return fallbackResponse();
}
// 继续走缓存逻辑
return queryUserById(Long.valueOf(key));
}
7.7 RedisBloom 布隆过滤器使用(防穿透)
创建 Bloom 过滤器
BF.RESERVE user_filter 0.01 1000000
添加数据
BF.ADD user_filter user:1001
定时刷新布隆过滤器
@Scheduled(fixedRate = 3600_000)
public void refreshBloom() {
List<Long> userIds = userMapper.selectAllUserIds();
for (Long id : userIds) {
stringRedisTemplate.execute((RedisCallback<Object>) connection ->
connection.execute("BF.ADD", "user_filter".getBytes(), ("user:" + id).getBytes()));
}
}
7.8 消息队列异步刷新热点缓存(可选)
@RabbitListener(queues = "refresh-cache")
public void handleRefreshMsg(String key) {
User user = userMapper.selectById(extractIdFromKey(key));
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 10, TimeUnit.MINUTES);
localCache.put(key, user);
}
7.9 项目完整结构建议
src/
├── config/
│ ├── RedisConfig.java
│ └── CaffeineConfig.java
├── service/
│ └── CacheService.java
├── controller/
│ └── UserController.java
├── mq/
│ └── CacheRefreshConsumer.java
├── bloom/
│ └── BloomFilterUtil.java
└── limiter/
└── RateLimiterManager.java
7.10 综合效果测试与验证
问题类型 | 验证方法 | 是否防御成功 |
---|---|---|
穿透 | 连续请求不存在的用户 ID | ✅ 空值缓存 + 布隆过滤器拦截 |
击穿 | 高并发请求某热点用户信息,临近 TTL | ✅ 分布式锁 + 本地缓存抗压 |
雪崩 | 批量过期 key + Redis 崩溃模拟 | ✅ 多级缓存 + 限流 + 降级响应 |
第八章:Redis 缓存问题面试题解析(含源码与场景设计)
8.1 高频面试问题汇总表
面试问题编号 | 问题内容 |
---|---|
Q1 | 什么是缓存穿透?如何防止? |
Q2 | 什么是缓存雪崩?如何应对? |
Q3 | 什么是缓存击穿?如何防护? |
Q4 | 多级缓存系统中如何保持数据一致性? |
Q5 |
| 如何使用布隆过滤器避免缓存穿透? |
| Q6 | 延迟双删策略具体怎么实现? |
| Q7 | 分布式锁如何避免击穿?与 Redisson 有何区别? |
| Q8 | 如何监控缓存健康状况?有哪些核心指标? |
| Q9 | Redis 的过期策略有哪些?如何选择? |
| Q10 | 如何防止缓存和数据库“双写不一致”? |
8.2 面试题详解与答案
Q1:什么是缓存穿透?如何防止?
- 定义:查询不存在的数据,缓存未命中,数据库也无,造成每次请求都打 DB。
- 成因:参数非法、大量恶意请求。
防御方法:
- 空值缓存:null/"" 缓存一段时间。
- 布隆过滤器:提前判断 key 是否存在。
- 验证层参数合法性校验。
Q2:什么是缓存雪崩?如何防止?
- 定义:大量缓存集中过期,Redis 压力骤增,大量请求打到 DB。
- 成因:统一设置了相同 TTL,或者 Redis 整体故障。
解决方案:
- 设置随机过期时间
- 多级缓存(本地 + Redis)
- 限流 / 熔断 / 降级机制
- Redis Cluster + Sentinel 容灾架构
Q3:什么是缓存击穿?如何防护?
- 定义:某个热点 key 突然失效,恰逢高并发访问时,大量请求同时击穿 DB。
解决方案:
- 分布式锁互斥构建缓存
- 逻辑过期 + 异步刷新
- 设置永不过期 + 定时后台刷新
Q4:多级缓存如何保持一致性?
- 更新数据库 → 删除 Redis 缓存 → 删除本地缓存
- 延迟双删策略
- MQ 异步刷新本地缓存
- 使用版本号 + TTL 标记缓存失效
Q5:布隆过滤器实现原理?
- 位图结构 + 多哈希函数
- 查询 key 是否在集合中,存在返回“可能存在”,不存在返回“绝对不存在”
- 有误判率、无漏判率
Q6:延迟双删策略实现流程?
- 更新数据库
- 删除缓存(第一次)
- 等待 500ms
- 再次删除缓存(第二次兜底)
Q7:Redis 分布式锁机制?
setIfAbsent
设置锁- Redisson 提供自动续期和重入锁
Q8:缓存健康监控指标有哪些?
- keyspace\_hits
- keyspace\_misses
- memory\_used\_bytes
- evicted\_keys
- connected\_clients
- expired\_keys
- slowlog
Q9:Redis 过期策略有哪些?
- noeviction
- allkeys-lru
- volatile-lru
- allkeys-random
Q10:如何保证缓存与数据库的一致性?
- 推荐流程:更新数据库 → 删除缓存
- 延迟双删 + 分布式锁
- MQ 异步刷新策略
- 版本号、时间戳避免旧数据覆盖新数据
第九章:Redis 缓存系统的性能优化建议与生产经验总结
9.1 Redis Key 设计规范
- 使用英文冒号分隔,格式统一
- 保持 key 长度合理 (<128 byte)
- 避免特殊字符
9.2 TTL(缓存时间)设计原则
- 不宜过短或过长,结合数据特点
- 设置随机过期时间避免雪崩
9.3 缓存系统热点 Key 检测实践
- 使用 redis-cli monitor(开发)
- 使用 redis-cli --hotkeys
- 利用 Prometheus + Grafana 监控
9.4 生产系统缓存常见问题排查流程
- 查看命中率指标
- 查询慢日志(slowlog)
9.5 Redis 生产配置优化建议
配置项 | 推荐值 |
---|---|
appendonly | yes |
appendfsync | everysec |
maxmemory-policy | volatile-lru / allkeys-lru |
tcp-keepalive | 60 |
save | 900 1, 300 10, 60 10000 |
timeout | 300 |
latency-monitor-threshold | 500 ms |
9.6 Redis 故障处理真实案例分析
案例一:缓存雪崩引发数据库连接池耗尽
- 统一 TTL 导致缓存集中失效
- DB 承受不了并发,连接池耗尽
- 解决方案:分散 TTL + 预热 + 限流降级
案例二:缓存击穿导致接口卡顿
- 热点 key 失效,QPS 突增打 DB
- 使用分布式锁 + 逻辑过期 + 异步刷新优化
9.7 Redis 缓存调优 Checklist
- 防穿透:布隆过滤器 + 空值缓存
- 防击穿:分布式锁 + 逻辑过期
- 防雪崩:随机 TTL + 限流 + 降级
- 缓存一致性:更新 DB → 延迟双删缓存
- 监控告警:命中率、QPS、慢查询、内存使用等
- 容灾切换:Sentinel / Cluster
- 多级缓存设计:本地 + Redis