高并发场景下的缓存最佳实践
详解缓存穿透、缓存击穿、缓存雪崩的解决方案,介绍布隆过滤器、分布式锁、缓存一致性等高级模式,结合 Spring Data Redis 提供完整代码示例。
在高并发系统中,数据库往往是性能瓶颈。Redis 作为内存数据库,读写性能是 MySQL 的 10-50 倍。通过合理的缓存策略,可以显著降低数据库压力,提升系统响应速度。
生产环境中,缓存使用不当会引发三种典型问题:
1. 缓存穿透 — 查询一个不存在的数据,缓存和数据库都无法命中,导致每次请求都打到数据库。
// 解决方案:缓存空值 + 布隆过滤器
@Component
public class CachePenetrationSolution {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RBloomFilter<String> bloomFilter;
public User getUser(Long id) {
String key = "user:" + id;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
// 缓存命中(包括空值标记 "null")
return "null".equals(cached) ? null : JSON.parseObject(cached, User.class);
}
// 布隆过滤器拦截
if (!bloomFilter.contains(key)) {
// 数据一定不存在,直接返回
return null;
}
// 查询数据库
User user = userMapper.selectById(id);
if (user == null) {
// 缓存空值,过期时间设置较短
redisTemplate.opsForValue().set(key, "null", 60, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
}
return user;
}
}
2. 缓存击穿 — 热点数据在缓存失效的瞬间,大量并发请求同时打到数据库。
// 解决方案:互斥锁 + 逻辑过期
@Component
public class CacheBreakdownSolution {
@Autowired
private RedissonClient redissonClient;
public User getHotUser(Long id) {
String key = "user:" + id;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
RedisData redisData = JSON.parseObject(json, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
// 逻辑未过期,直接返回
return JSON.parseObject(redisData.getData(), User.class);
}
// 逻辑已过期,尝试获取锁重建缓存
RLock lock = redissonClient.getLock("lock:user:" + id);
if (lock.tryLock()) {
try {
// 双重检查
json = redisTemplate.opsForValue().get(key);
redisData = JSON.parseObject(json, RedisData.class);
if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
return JSON.parseObject(redisData.getData(), User.class);
}
// 重建缓存
User user = userMapper.selectById(id);
cacheUserWithLogicalExpire(id, user);
return user;
} finally {
lock.unlock();
}
}
}
// 未获取到锁,返回旧数据(保证可用性)
return JSON.parseObject(JSON.parseObject(json, RedisData.class).getData(), User.class);
}
}
3. 缓存雪崩 — 大量缓存同时失效或 Redis 宕机,导致数据库瞬时压力过大。
// 解决方案:过期时间加随机值 + 多级缓存
@Component
public class CacheAvalancheSolution {
@Autowired
private CacheManager caffeineCacheManager;
public User getUserWithMultiLevel(Long id) {
// L1: Caffeine 本地缓存(进程内,毫秒级)
Cache localCache = caffeineCacheManager.getCache("userLocal");
User user = localCache.get(id, User.class);
if (user != null) return user;
// L2: Redis 分布式缓存(微秒级)
String key = "user:" + id;
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
user = JSON.parseObject(json, User.class);
localCache.put(id, user); // 回填本地缓存
return user;
}
// L3: 数据库
user = userMapper.selectById(id);
if (user != null) {
// 过期时间加随机值,避免同时失效
long expire = 30 * 60 + ThreadLocalRandom.current().nextInt(300);
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), expire, TimeUnit.SECONDS);
localCache.put(id, user);
}
return user;
}
}
Redisson 提供了可重入、可续期的分布式锁,是 Redis 实现分布式锁的最佳实践:
@Component
public class DistributedLockDemo {
@Autowired
private RedissonClient redissonClient;
public void deductStock(String productId, int count) {
RLock lock = redissonClient.getLock("stock:" + productId);
try {
// 尝试获取锁,最多等待 10 秒,锁持有时长 30 秒(看门狗自动续期)
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!acquired) {
throw new BusinessException("获取库存锁失败,请稍后重试");
}
int stock = getStock(productId);
if (stock < count) {
throw new BusinessException("库存不足");
}
updateStock(productId, stock - count);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("操作被中断");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
缓存与数据库的数据一致性是分布式系统的经典难题。推荐采用 Cache-Aside 模式:
// 读:先查缓存,未命中再查数据库并回填
public User getById(Long id) {
String key = "user:" + id;
User user = cache.get(key);
if (user == null) {
user = userMapper.selectById(id);
if (user != null) cache.set(key, user, 30, MINUTES);
}
return user;
}
// 写:先更新数据库,再删除缓存(非更新缓存)
@Transactional
public void update(User user) {
userMapper.updateById(user);
cache.delete("user:" + user.getId());
}
// 删除:先删数据库,再删缓存
@Transactional
public void delete(Long id) {
userMapper.deleteById(id);
cache.delete("user:" + id);
}