单用redis setex无法防击穿,因为其仅设置键值和过期时间,无并发控制机制;当热点key过期瞬间,多个线程同时miss缓存并并发查库、重复写缓存,导致db压力激增与资源浪费。

为什么单用Redis setex 无法防击穿
缓存击穿的本质是:某个热点 key 刚过期,瞬间大量并发请求同时发现缓存 miss,全部涌向数据库。哪怕你用了 redisTemplate.opsForValue().setex(),只要没加锁或协调机制,多个线程仍会并行执行 DB 查询 + 再写缓存,造成重复加载和 DB 压力 spikes。
常见错误现象包括:
- 日志里同一秒内出现几十次
SELECT * FROM user WHERE id = ? - Redis 中该 key 的 TTL 刚归零,监控看到 DB QPS 突增 5–10 倍
- 即使加了空值缓存,对“刚过期的热点 key”也无效——空值还没来得及写,DB 就已被打爆
Guava LoadingCache + Redis 双层兜底怎么配
核心思路不是“替代 Redis”,而是用 Guava 在应用层做本地轻量级协调:让第一个线程去查 DB 并回写 Redis,其余线程阻塞等待结果,避免重复穿透。
关键配置要点:
-
CacheBuilder.newBuilder().maximumSize(1000):限制本地缓存大小,防内存溢出 -
.expireAfterWrite(10, TimeUnit.MINUTES):比 Redis TTL 略短(比如 Redis 设 15 分钟),确保本地不会长期 stale -
.refreshAfterWrite(8, TimeUnit.MINUTES):在过期前自动异步刷新,缓解击穿风险 - 加载逻辑中必须调用
redisTemplate.opsForValue().set(),保证 Redis 缓存同步更新
示例片段(非完整类):
private final LoadingCache<string user> userCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(8, TimeUnit.MINUTES)
.build(new CacheLoader<string user>() {
@Override
public User load(String key) throws Exception {
Long userId = Long.valueOf(key.replace("user:", ""));
User user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set("user:" + userId, user, 15, TimeUnit.MINUTES);
} else {
// 空值也写 Redis,防穿透
redisTemplate.opsForValue().set("user:" + userId, "", 2, TimeUnit.MINUTES);
}
return user;
}
});</string></string>
Redis 过期时间必须加随机偏移
如果所有用户缓存都设成 15 * 60 秒固定 TTL,高峰期一到,大量 key 同时失效,Guava 的本地 refresh 也救不了——因为本地缓存本身也会批量过期,触发集中 reload。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
正确做法是让每个 key 的 TTL 在合理范围内抖动:
- 基础 TTL 设为 900 秒(15 分钟)
- 加上 ±120 秒随机偏移:
900 + ThreadLocalRandom.current().nextInt(-120, 121) - 对应代码中写入 Redis 时用
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS)
这个抖动不破坏业务语义,但能显著摊平过期洪峰。
Guava 本地缓存失效后,如何避免 Redis 也被误删
容易被忽略的一点:Guava 的 invalidate() 或 invalidateAll() 只清本地,但如果你在业务里写了“缓存失效就顺手删 Redis”,就可能引发连锁问题——比如管理员手动刷新某用户数据,触发 cache.invalidate("user:1001"),结果 Redis 里那个 key 也被删了,下次请求直接击穿 DB。
安全做法是明确分离职责:
- Guava 只管本地加载、超时、刷新
- Redis 的写入/删除必须显式调用,且只在 load() 方法里写、在 DB 更新成功后删
- 禁止在 Guava 回调外调用
redisTemplate.delete()
复杂点在于:当 DB 数据真正变更(如用户改名),你得主动触发一次 userCache.refresh("user:1001"),而不是删——这样既更新本地,又通过 load() 逻辑把新值刷进 Redis。










