分布式缓存
约 889 字大约 3 分钟
布欧-Lewyon
2026-05-15
Redis 最核心的应用场景——缓存。本节覆盖缓存穿透、击穿、雪崩的解决方案。
缓存穿透
请求的数据在缓存和数据库中都不存在,大量请求直接打到数据库。
解决方案:缓存空值
@Service
public class UserCacheService {
@Autowired
private StringRedisTemplate redis;
public User getUser(Long id) {
// 1. 查缓存
String json = redis.opsForValue().get("user:" + id);
if (json != null) {
if ("NULL".equals(json)) return null; // 缓存空值
return JSON.parseObject(json, User.class);
}
// 2. 缓存未命中,查数据库
User user = userMapper.findById(id);
// 3. 缓存结果(空值也缓存,防穿透)
if (user == null) {
redis.opsForValue().set("user:" + id, "NULL", 60, TimeUnit.SECONDS);
} else {
redis.opsForValue().set("user:" + id, JSON.toJSONString(user),
3600, TimeUnit.SECONDS);
}
return user;
}
}解决方案:布隆过滤器
@Component
public class BloomFilterService {
@Autowired
private RedissonClient redisson;
private RBloomFilter<Long> bloomFilter;
@PostConstruct
public void init() {
bloomFilter = redisson.getBloomFilter("user:bloom");
bloomFilter.tryInit(1000000L, 0.01); // 100 万期望容量,1% 误判率
}
// 初始化时加载所有用户 ID
public void loadAllUserIds() {
List<Long> allIds = userMapper.findAllIds();
allIds.forEach(bloomFilter::add);
}
public User getUserWithBloom(Long id) {
// 布隆过滤:不存在则直接返回
if (!bloomFilter.contains(id)) {
return null;
}
// 存在则查缓存/数据库
return getUserFromCacheOrDB(id);
}
}缓存击穿
一个热点 key 过期,大量并发请求同时打到数据库。
解决方案:互斥锁
public User getUserWithLock(Long id) {
String cacheKey = "user:" + id;
String json = redis.opsForValue().get(cacheKey);
if (json != null) {
return JSON.parseObject(json, User.class);
}
// 缓存未命中 → 加锁(只让一个线程查 DB)
String lockKey = "lock:user:" + id;
Boolean locked = redis.opsForValue()
.setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 双重检查
json = redis.opsForValue().get(cacheKey);
if (json != null) {
return JSON.parseObject(json, User.class);
}
User user = userMapper.findById(id);
if (user != null) {
redis.opsForValue().set(cacheKey,
JSON.toJSONString(user), 3600, TimeUnit.SECONDS);
}
return user;
} finally {
redis.delete(lockKey);
}
} else {
// 其他线程等待
Thread.sleep(100);
return getUserWithLock(id);
}
}缓存雪崩
大量 key 同时过期,大量请求落到数据库。
解决方案:过期时间随机化
// 设置缓存时,在基础过期时间上增加随机偏移
int baseExpire = 3600; // 1 小时
int randomDelta = new Random().nextInt(600); // 0-10 分钟随机
redis.opsForValue().set(key, value,
baseExpire + randomDelta, TimeUnit.SECONDS);解决方案:多级缓存
@Service
public class MultiLevelCacheService {
// L1:本地缓存(Caffeine)
@Autowired
private Cache<String, String> localCache;
// L2:Redis 分布式缓存
@Autowired
private StringRedisTemplate redis;
public User getUser(Long id) {
String key = "user:" + id;
// L1 本地缓存
String json = localCache.getIfPresent(key);
if (json != null) return JSON.parseObject(json, User.class);
// L2 Redis 缓存
json = redis.opsForValue().get(key);
if (json != null) {
localCache.put(key, json); // 回填 L1
return JSON.parseObject(json, User.class);
}
// L3 数据库
User user = userMapper.findById(id);
if (user != null) {
String userJson = JSON.toJSONString(user);
redis.opsForValue().set(key, userJson, 3600, TimeUnit.SECONDS);
localCache.put(key, userJson);
}
return user;
}
}Spring Cache 注解
@Service
@CacheConfig(cacheNames = "users")
public class UserService {
@Cacheable(key = "#id", unless = "#result == null")
public User getById(Long id) {
return userMapper.findById(id);
}
@CachePut(key = "#user.id")
public User update(User user) {
userMapper.update(user);
return user;
}
@CacheEvict(key = "#id")
public void delete(Long id) {
userMapper.delete(id);
}
}小结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缓存穿透 | 缓存+DB 都无 | 缓存空值 / 布隆过滤器 |
| 缓存击穿 | 热点 key 过期 | 互斥锁 / 永不过期 |
| 缓存雪崩 | 大量 key 同时过期 | 过期时间随机化 / 多级缓存 |
- 空值缓存
"NULL"+ 短过期时间防止穿透。 SETNX互斥锁防止击穿(Redisson 分布式锁更健壮)。baseExpire + random防雪崩。
上一节:Redisson 分布式组件 下一节:分布式锁与限流器
