1. Redis缓存三兄弟:穿透、击穿与雪崩实战解析
作为Java后端开发者,Redis的缓存问题是面试必考点。我在实际项目中遇到过多次缓存异常场景,今天系统梳理下这三个经典问题的本质区别和解决方案。
1.1 缓存穿透:不存在的Key攻击
典型场景:某次大促期间,监控突然报警显示数据库CPU飙升至90%。排查发现大量请求在查询不存在的商品ID,这些请求直接穿透Redis打到数据库。
核心特征:
- 请求的Key在Redis和MySQL中都不存在
- 可能是恶意攻击或业务逻辑缺陷导致
- 每次请求都会直达数据库
解决方案对比:
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 空值缓存 | 对不存在的Key也缓存空值(如"NULL") | 实现简单 | 可能缓存大量无效Key | 数据量小的场景 |
| 布隆过滤器 | 使用Bit数组+多个哈希函数 | 空间效率极高 | 存在误判率 | 海量数据场景 |
布隆过滤器实战配置:
java复制// Redisson布隆过滤器配置示例
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("productFilter");
// 预期插入100万条数据,误判率0.1%
bloomFilter.tryInit(1000000L, 0.001);
经验:布隆过滤器的误判率需要根据业务容忍度设置。我们电商系统设置为0.5%,实际测试中误判率约为0.3%-0.7%。
1.2 缓存击穿:热点Key失效风暴
踩坑案例:某明星商品在秒杀开始时,由于缓存同时失效,导致QPS瞬间从2000飙升至2万,数据库连接池被打满。
关键区别:
- Key在数据库中存在
- 是热点数据
- 缓存刚好过期时突发高并发
解决方案对比:
| 方案 | 实现原理 | 一致性 | 性能 | 复杂度 |
|---|---|---|---|---|
| 互斥锁 | 使用SETNX实现分布式锁 | 强一致 | 较低 | 中等 |
| 逻辑过期 | 缓存永不过期,后台异步更新 | 最终一致 | 高 | 较高 |
互斥锁实现细节:
java复制public Product getProduct(Long id) {
String lockKey = "product:" + id;
try {
// 尝试获取分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
Thread.sleep(100); // 短暂等待后重试
return getProduct(id);
}
// 查询数据库
Product product = db.query(id);
// 更新缓存
redisTemplate.opsForValue()
.set("cache:"+id, product, 1, TimeUnit.HOURS);
return product;
} finally {
redisTemplate.delete(lockKey);
}
}
避坑指南:锁的过期时间要大于业务执行时间但不宜过长,我们设置为30秒并配合看门狗机制续期。
1.3 缓存雪崩:集体失效灾难
事故复盘:某次全站缓存设置为2小时过期,结果在凌晨流量低谷时同时失效,早高峰时数据库直接崩溃。
预防措施:
- 过期时间随机化:基础过期时间+随机偏移量(如1小时±10分钟)
- 多级缓存架构:本地缓存+Redis+数据库
- 服务熔降级:Hystrix或Sentinel保护数据库
过期时间优化方案:
java复制// 基础过期时间1小时,随机增加0-30分钟
int expireTime = 3600 + new Random().nextInt(1800);
redisTemplate.opsForValue()
.set(key, value, expireTime, TimeUnit.SECONDS);
2. Redis持久化与数据淘汰策略
2.1 RDB与AOF深度对比
在金融级系统中,我们采用混合持久化方案:
| 维度 | RDB | AOF | 混合模式 |
|---|---|---|---|
| 恢复速度 | 快(二进制加载) | 慢(命令重放) | 快(RDB基础+AOF增量) |
| 数据安全 | 可能丢失最后一次快照后的数据 | 最多丢失1秒数据 | 同AOF |
| 文件大小 | 小(压缩二进制) | 大(文本命令) | 中等 |
| 写性能 | 高(后台fork进程) | 中(依赖fsync配置) | 中 |
配置建议:
conf复制# redis.conf关键配置
save 900 1 # 15分钟至少1个key变化
save 300 10 # 5分钟至少10个key变化
appendonly yes # 开启AOF
aof-use-rdb-preamble yes # 开启混合模式
2.2 内存淘汰策略实战选择
策略对比表:
| 策略 | 算法 | 特点 | 适用场景 |
|---|---|---|---|
| allkeys-lru | 最近最少使用 | 平衡性好 | 通用场景 |
| volatile-lru | LRU只对过期Key | 保留永久Key | 需要保留部分数据 |
| allkeys-lfu | 最不经常使用 | 精准但耗CPU | 热点数据明显 |
| volatile-ttl | 剩余存活时间 | 快速释放空间 | 临时数据场景 |
监控指标:
bash复制# 查看内存和淘汰情况
redis-cli info memory
# 输出示例:
used_memory_human:1.2G
maxmemory_human:2.0G
evicted_keys:120 # 已淘汰Key数量
调优经验:我们电商系统使用allkeys-lru,配合监控发现当内存使用达80%时开始出现性能抖动,因此设置告警阈值为75%。
3. 分布式锁的魔鬼细节
3.1 Redisson锁实现原理
加锁流程:
- 客户端生成唯一锁ID(UUID+线程ID)
- 执行Lua脚本保证原子性:
lua复制if redis.call('exists', KEYS[1]) == 0 then redis.call('hset', KEYS[1], ARGV[2], 1) redis.call('pexpire', KEYS[1], ARGV[1]) return 1 end - 启动看门狗线程定期续期(默认30秒续期到30秒)
锁续期机制:
java复制// Redisson看门狗核心逻辑
private void scheduleExpirationRenewal() {
Thread task = new Thread(() -> {
while (true) {
// 每10秒续期一次
Thread.sleep(10000);
// 执行Lua脚本延长锁时间
evalWriteAsync(...);
}
});
task.start();
}
3.2 集群环境下的锁风险
主从切换问题复现:
- 线程A在master加锁成功
- master宕机,锁未同步到slave
- slave升级为master后,线程B也能加锁
解决方案对比:
| 方案 | 原理 | 可用性 | 性能 | 实现复杂度 |
|---|---|---|---|---|
| RedLock | 需在多数节点加锁成功 | 高 | 差(N次网络开销) | 高 |
| Zookeeper | 利用ZAB一致性协议 | 极高 | 一般 | 低 |
实际选择:除非金融级场景,否则建议接受极低概率的锁失效,通过业务幂等性弥补。我们订单系统使用Redisson单节点锁+3秒自动过期,两年未出现业务异常。
4. Redis集群架构实战
4.1 主从同步全流程解析
全量同步时序图:
- slave发送psync ? -1
- master执行bgsave生成RDB
- master将RDB发送给slave
- slave清空数据后加载RDB
- master将缓冲区的写命令发送给slave
增量同步关键点:
- replication backlog缓冲区(默认1MB)
- master和slave维护相同的replid和offset
- 网络中断恢复后根据offset继续同步
监控命令:
bash复制redis-cli info replication
# 关键指标:
master_repl_offset:3265
slave_repl_offset:3265 # 差值过大说明同步延迟
4.2 哨兵模式部署要点
推荐部署方案:
- 至少3个哨兵节点(避免脑裂)
- 哨兵与Redis分机器部署
- 配置合理的down-after-milliseconds(通常30秒)
哨兵配置示例:
conf复制sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
4.3 分片集群数据分布
哈希槽分配原理:
- 总槽数:16384
- 每个节点负责部分槽范围
- Key通过CRC16(key) % 16384计算槽位
集群管理命令:
bash复制# 查看槽分配
redis-cli cluster slots
# 手动迁移槽(运维使用)
redis-cli cluster reshard <host>:<port>
5. Redis高性能之谜
5.1 单线程架构的优势
性能关键点:
- 纯内存操作(100ns级响应)
- 避免锁竞争和上下文切换
- 顺序执行无并发问题
实测对比:
| 操作 | 单线程QPS | 多线程QPS |
|---|---|---|
| GET | 120,000 | 110,000(线程切换开销) |
| SET | 100,000 | 95,000 |
5.2 I/O多路复用实现
epoll模型工作流程:
- 创建epoll实例(epoll_create)
- 注册socket事件(epoll_ctl)
- 等待事件就绪(epoll_wait)
- 处理就绪事件
Redis网络事件循环:
c复制// 简化版事件循环
while(1) {
events = epoll_wait(epfd, events, MAX_EVENTS, -1);
for(i = 0; i < events; i++) {
if(events[i].data.fd == serverSocket) {
acceptNewConnection();
} else {
processClientCommand(events[i].data.fd);
}
}
}
5.3 多线程的合理使用
Redis6.0线程模型:
- I/O线程:处理网络读写(默认4个)
- 主线程:执行命令、处理时间事件
- 后台线程:惰性删除、AOF刷盘
配置建议:
conf复制io-threads 4 # 通常设为CPU核数-1
io-threads-do-reads yes # 开启读多线程
在千万级QPS的社交系统优化中,开启多线程后网络处理性能提升了40%,但CPU核心数不足时反而会下降,需要根据实际负载测试调整。