第93篇:Redis实战应用:缓存策略与分布式锁(2026版)
📌 系列导航 :《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第92篇:Redis高级特性深度解析 |
➡️ 下一篇:第94篇:Redis面试高频题
一、核心知识点
- 缓存穿透:概念、危害、解决方案(布隆过滤器、缓存空对象)
- 缓存击穿:概念、危害、解决方案(互斥锁、逻辑过期)
- 缓存雪崩:概念、危害、解决方案(随机TTL、高可用集群、熔断降级)
- 分布式锁 :
SETNX手写锁的问题、Redisson 实现原理(看门狗机制) - Spring Boot 整合 Redis:配置、序列化、缓存注解
- 多级缓存架构:本地缓存(Caffeine)+ Redis,热点 Key 治理
二、通俗讲解(1分钟开心学)
1. 缓存三大杀手
| 术语 | 比喻 | 一句话解释 |
|---|---|---|
| 缓存穿透 | 有人专门查你数据库里没有的身份证号 | 请求的数据不存在于缓存和数据库,直接穿透到 DB |
| 缓存击穿 | 明星过生日,粉丝瞬间涌入蛋糕店 | 某个热点 key 过期瞬间,大量并发请求打爆 DB |
| 缓存雪崩 | 多家店同时关门,顾客全涌向唯一开门的店 | 大量 key 同时过期或 Redis 宕机,DB 瞬间压力暴增 |
2. 分布式锁------秒杀防超卖
多台服务器同时修改同一数据(如库存)时需要加锁,保证同一时刻只有一个线程能操作。
生活类比 :
小区只有一个篮球,一群孩子想玩。谁先拿到球谁玩,玩完放回。Redisson 就是那个"自动计时、公平分配"的智能篮球架。
三、实操代码案例 + 场景说明
项目环境 :Spring Boot 2.7+,依赖
spring-boot-starter-data-redis、redisson-spring-boot-starter
3.1 解决缓存穿透(布隆过滤器)
布隆过滤器原理:位数组 + 多个哈希函数。一个 key 存在时可能误判,不存在时一定准确。
方案一:Guava 内存布隆过滤器(单机/测试用)
java
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
@Component
public class GuavaBloomFilterService {
private BloomFilter<String> bloomFilter;
private static final int EXPECTED_INSERTIONS = 1000000;
private static final double FPP = 0.01;
@PostConstruct
public void init() {
bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),
EXPECTED_INSERTIONS, FPP);
loadAllProductIds();
}
public boolean mightExist(String key) { return bloomFilter.mightContain(key); }
}
⚠️ 注意 :Guava 布隆过滤器数据存储在 JVM 内存中,服务重启后数据会丢失,且无法在集群间共享。生产环境建议使用方案二。
方案二:Redisson 分布式布隆过滤器(生产推荐)
Redisson 的 RBloomFilter 底层基于 Redis 的 BitMap,数据持久化、可跨 JVM 共享。
xml
<!-- Redisson 已引入,无需额外依赖 -->
java
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
@Component
public class RedissonBloomFilterService {
@Autowired
private RedissonClient redissonClient;
private RBloomFilter<String> bloomFilter;
private static final String FILTER_NAME = "product:bloom";
@PostConstruct
public void init() {
bloomFilter = redissonClient.getBloomFilter(FILTER_NAME);
// 初始化:预计插入100万,误判率1%
bloomFilter.tryInit(1000000L, 0.01);
// 预加载数据(仅首次执行)
if (bloomFilter.count() == 0) {
loadAllProductIds();
}
}
public boolean mightExist(String id) {
return bloomFilter.contains(id);
}
public void add(String id) {
bloomFilter.add(id);
}
private void loadAllProductIds() {
// 从 DB 批量加载所有商品ID
List<String> ids = productDao.getAllIds();
ids.forEach(bloomFilter::add);
}
}
查询逻辑(两种方案通用):
java
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable String id) {
// 1. 布隆过滤器拦截
if (!bloomFilterService.mightExist(id)) {
return null;
}
// 2. 查缓存、回写 DB(同前文)
// ...
}
3.2 解决缓存击穿
方案一:互斥锁(简单有效,推荐)
参见前文的 getProductWithLock 实现,使用 Redisson 或 Redis setIfAbsent。
方案二:逻辑过期(高性能,容忍短暂不一致)
适用场景:热点数据允许短时间内不一致(如商品详情、配置信息)。
核心思想 :缓存中存储 value + expireTime,不设置 Redis 的 TTL。读取时判断是否逻辑过期,若过期则异步去 DB 更新,其他请求直接返回旧值。
java
public class ProductWithLogicExpire {
private Product product;
private Long expireTime; // 逻辑过期时间戳
}
public Product getProductLogicExpire(String id) {
String key = "product:logic:" + id;
ProductWithLogicExpire wrapper = (ProductWithLogicExpire) redisTemplate.opsForValue().get(key);
if (wrapper == null) {
// 缓存不存在,走互斥锁更新(第一次加载)
return loadFromDBAndSetCache(id);
}
// 判断是否逻辑过期
if (wrapper.getExpireTime() > System.currentTimeMillis()) {
return wrapper.getProduct(); // 未过期,直接返回
}
// 逻辑过期,尝试获取互斥锁去异步更新
String lockKey = "lock:product:refresh:" + id;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 异步线程去 DB 加载并更新缓存
CompletableFuture.runAsync(() -> {
try {
Product newProduct = productDao.findById(id);
// 重新设置缓存,逻辑过期时间 = 当前时间 + 随机TTL
ProductWithLogicExpire newWrapper = new ProductWithLogicExpire();
newWrapper.setProduct(newProduct);
newWrapper.setExpireTime(System.currentTimeMillis() + 3600 * 1000 + new Random().nextInt(600) * 1000);
redisTemplate.opsForValue().set(key, newWrapper);
} finally {
redisTemplate.delete(lockKey);
}
});
}
// 返回旧数据(容忍短暂不一致)
return wrapper.getProduct();
}
优缺点对比:
- 互斥锁:强一致,但会阻塞部分请求,适合写少读多或一致性要求高。
- 逻辑过期:无阻塞,性能极高,但可能读到旧数据,适合如"商品库存"以外的展示类数据。
3.3 解决缓存雪崩(随机 TTL)
java
int baseTTL = 3600; // 基础1小时
Random random = new Random();
int ttl = baseTTL + random.nextInt(600); // 增加0~600秒随机值
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
3.4 Redisson 分布式锁(生产标准)
扣库存示例(秒杀场景):
java
@Service
public class SeckillService {
@Autowired
private RedissonClient redissonClient;
public boolean deductStock(String productId, int quantity) {
String lockKey = "lock:seckill:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待3秒,锁自动释放时间30秒(看门狗会续期)
boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (!locked) return false;
// 业务逻辑:查库存、扣减
String stockKey = "stock:" + productId;
Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
if (stock == null || stock < quantity) return false;
redisTemplate.opsForValue().decrement(stockKey, quantity);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Redisson 看门狗原理:锁默认30秒过期,若业务未完成,看门狗每10秒自动续期,避免锁过期提前释放。
3.5 多级缓存架构(Caffeine + Redis)
java
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
@Component
public class MultiLevelCache {
private Cache<String, Object> localCache;
@PostConstruct
public void init() {
localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(60, TimeUnit.SECONDS)
.build();
}
public Object get(String key) {
// 1. 本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) return value;
// 2. Redis
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// 3. DB 查询并回写
value = queryDB(key);
if (value != null) {
redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
localCache.put(key, value);
}
return value;
}
}
四、生产环境避坑清单
| 分类 | 错误/误区 | 后果 | 正确做法 |
|---|---|---|---|
| 缓存穿透 | 使用 Guava 布隆过滤器生产环境部署 | 服务重启数据丢失,集群不共享 | 生产用 Redisson 的 RBloomFilter(持久化) |
| 缓存穿透 | 不校验请求参数合法性 | 恶意请求打爆 DB | 布隆过滤器 + 缓存空对象 + 参数校验 |
| 缓存击穿 | 仅用互斥锁,忽略逻辑过期 | 高并发下排队,性能下降 | 结合场景:强一致用互斥锁;容忍短暂不一致用逻辑过期 |
| 缓存雪崩 | 所有 key 相同 TTL | 大量 key 同时失效 | 随机 TTL + Redis 集群 |
| 分布式锁 | 手写 SETNX 未设置过期时间 |
死锁 | 使用 Redisson(自动续期) |
| 分布式锁 | 释放锁未校验持有者 | 误删其他线程的锁 | Redisson 自动处理 |
| 多级缓存 | 本地缓存和 Redis 数据不一致 | 脏数据 | 监听 Redis 变更消息,主动失效本地缓存 |
五、面试高频考点
Q1:布隆过滤器的原理与误判率?
位数组 + 多个哈希函数。不存在一定准确,存在可能误判。误判率与位数组大小和哈希函数个数有关,可配置(如 1%)。生产推荐 Redisson 的 RBloomFilter(持久化、分布式)。
Q2:Redisson 分布式锁的看门狗机制?
锁默认过期时间 30 秒,若业务未完成,看门狗线程每 10 秒检查并续期,业务完成后主动释放。避免了手动续期的复杂性。
Q3:缓存击穿互斥锁 vs 逻辑过期如何选择?
互斥锁保证强一致,适合库存、订单等;逻辑过期无阻塞、性能高,适合商品详情、配置等可容忍短暂不一致的数据。
Q4:多级缓存如何保证一致性?
① 更新时删除本地缓存和 Redis 缓存;② 使用 Canal + MQ 监听 MySQL 变更;③ 本地缓存设置较短 TTL。
Q5:Redis 分布式锁与 Zookeeper 锁对比?
Redis 性能更高,适合高并发场景;Zookeeper 保证强一致性,适合对可靠性要求极高的场景。Redisson 支持可重入、公平锁等高级特性。
六、练习题
- 代码题:基于 Redisson 实现一个可重入分布式锁,模拟 10 个线程同时扣减库存,保证最终库存正确。
- 设计题 :一个新闻 App 首页热点新闻缓存,要求不击穿 DB,且可容忍短暂不一致,请给出方案。 💡 思路:逻辑过期 + 互斥锁。缓存不带 TTL,记录逻辑过期时间,查询时若逻辑过期,获取锁去 DB 更新,其他请求返回旧数据。
- 故障排查:某系统上线后 CPU 飙升,发现大量线程在等待 Redisson 锁,可能原因是什么?
📊 你的学习进度
- 当前:第93篇 / 共108篇 · 进阶篇:缓存与消息队列(第91~96篇)
- ✅ 已完成:基础篇44篇 + 第91~93篇
- 📖 正在学:第93篇
- ⏳ 待学习:第94~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
💡 本专栏每篇都包含:避坑表 + 面试高频考点 + 练习题。每天30分钟,100天拿offer!
👉 下一篇文章预告
内容简介:汇总 30+ 道 Redis 大厂面试真题(含分布式锁、持久化选型、集群架构、缓存三大杀手、淘汰策略等),每道题附标准话术 + 加分回答,助你轻松应对面试。
💡 学完这篇,你将直接背诵面试答案,从"会用"到"碾压面试官"。
🎁 福利提醒:评论区留言"Redis面试"可获取《Redis 面试高频题 PDF》及 Redisson 配置模板。
📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注 ,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!