在Java后端面试中,面试题:如何解决Redis缓存击穿问题是高并发场景下的核心考察点——它不仅能考察求职者对缓存架构的理解深度,更能看穿你平衡“性能、一致性、成本”三者的能力。鳄鱼java社区的面试跟踪数据显示,能结合业务场景给出落地方案的求职者,高并发岗位通过率比只会背“互斥锁、逻辑过期”的高72%。
一、先拆解:面试题背后的考察点与缓存击穿的本质

很多求职者开口就说“用互斥锁或者逻辑过期”,但这完全没触及面试官的考察点。这个面试题的核心是要你先明确:缓存击穿和缓存穿透、雪崩的区别,以及它的业务痛点——缓存击穿是指单个热点key在过期瞬间,大量并发请求直接绕过缓存打向数据库,导致数据库瞬间压力过载,甚至宕机。比如电商秒杀的热点商品、直播平台的热门主播动态,一旦缓存过期,每秒可能有10万+请求直接冲击数据库。
面试官真正想考察的是:你是否能根据业务场景选择最优方案,而非堆砌技术名词。比如强一致性场景(如库存查询)和允许短暂不一致的场景(如商品详情),解决方案完全不同。鳄鱼java社区的JVM专家强调:面试中第一个明确区分缓存击穿与其他缓存问题的求职者,会立刻获得面试官的好感。
二、核心解决方案:从互斥锁到逻辑过期的落地实现
针对缓存击穿,业界有4种主流落地方案,每种方案都有明确的适用场景,下面结合代码和鳄鱼java的实战案例讲解:
1. 互斥锁方案:强一致性场景的首选
原理:当缓存过期时,用Redis的setnx(或setIfAbsent)实现分布式互斥锁,只允许一个线程去查数据库更新缓存,其他线程等待重试,避免大量请求打数据库。
示例代码(Spring Boot+Redis):
@Autowired private StringRedisTemplate redisTemplate;public String getStock(String productId) { // 1. 先查缓存 String stock = redisTemplate.opsForValue().get("stock:" + productId); if (stock != null && !stock.isEmpty()) { return stock; }
// 2. 缓存空,尝试获取分布式锁 String lockKey = "lock:stock:" + productId; try { // 锁过期时间30s,避免线程异常导致锁无法释放 Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { // 3. 获取锁成功,查数据库 String dbStock = db.queryStock(productId); // 4. 更新缓存,设置过期时间 redisTemplate.opsForValue().set("stock:" + productId, dbStock, 3600, TimeUnit.SECONDS); return dbStock; } else { // 5. 获取锁失败,等待100ms重试 Thread.sleep(100); return getStock(productId); // 递归重试 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "0"; } finally { // 6. 释放锁 redisTemplate.delete(lockKey); }}
适用场景:强一致性业务(如库存、余额、订单状态),必须保证缓存和数据库的一致性。
优缺点:优点是数据一致性强,实现简单;缺点是高并发下会有线程等待,性能略低,存在死锁风险(需设置锁过期时间)。
2. 逻辑过期方案:读多写少场景的性能最优解
原理:缓存永不过期,而是在缓存值中嵌入逻辑过期时间。请求时判断是否过期,若未过期直接返回;若已过期,获取互斥锁后异步更新缓存,旧请求仍返回旧数据,不阻塞用户。
示例代码:
// 自定义缓存对象,包含业务数据和逻辑过期时间
@Data
static class CacheValue {
private String data;
private long expireTime; // 逻辑过期时间戳
}
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ExecutorService executorService;
public String getProductDetail(String productId) {
String cacheKey = "product:" + productId;
String cacheStr = redisTemplate.opsForValue().get(cacheKey);
if (cacheStr == null) {
// 缓存为空,直接返回默认值或查数据库(首次加载)
return db.queryProductDetail(productId);
}
// 反序列化缓存对象
CacheValue cacheValue = JSON.parseObject(cacheStr, CacheValue.class);
// 未逻辑过期,直接返回
if (System.currentTimeMillis() < cacheValue.getExpireTime()) {
return cacheValue.getData();
}
// 逻辑过期,异步更新缓存
String lockKey = "lock:product:" + productId;
if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS))) {
executorService.submit(() -> {
try {
// 1. 查数据库获取最新数据
String newData = db.queryProductDetail(productId);
// 2. 构造新的缓存对象,设置逻辑过期时间(1小时后)
CacheValue newCacheValue = new CacheValue();
newCacheValue.setData(newData);
newCacheValue.setExpireTime(System.currentTimeMillis() + 3600 * 1000);
// 3. 更新缓存
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(newCacheValue));
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
});
}
// 返回旧数据,保证用户体验
return cacheValue.getData();
}
鳄鱼java社区的电商实战案例显示:采用逻辑过期方案后,秒杀商品的数据库请求量从每秒12万次降到每秒80次,服务响应时间从450ms压缩至60ms。
3. 热点key永不过期+主动更新:终极预防方案
原理:对于访问量前10的热点key,设置为永不过期,同时通过定时任务或消息队列主动更新缓存。比如用Arthas监控缓存访问次数,将访问量Top N的key标记为热点key,后台每10分钟主动更新一次数据。
适用场景:秒杀商品、热门直播、明星动态等访问量极高且更新频率低的场景。
4. 本地缓存+分布式缓存:双层防护
原理:用Caffeine做本地缓存(每个服务节点本地存储热点数据),请求时先查本地缓存,未命中再查Redis,最后查数据库。即使Redis缓存过期,本地缓存仍能挡住大部分请求,避免冲击数据库。
鳄鱼java社区的测试数据显示:双层缓存架构能将数据库的请求量再降低70%,同时服务响应时间缩短至20ms以内。
三、方案选型:不同业务场景的最优选择
面试官更关注你是否能结合场景选方案,下面用表格对比各方案的核心指标:
| 方案 | 一致性 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 互斥锁 | 强一致 | 中 | 低 | 库存、余额等强一致场景 |
| 逻辑过期 | 最终一致 | 高 | 中 | 商品详情、主播动态等读多写少场景 |
版权声明
本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。





