引言
在现代分布式系统架构中,Redis作为高性能的内存数据库,已经成为缓存系统的首选方案。然而,在高并发场景下,缓存系统往往会面临三大核心问题:缓存穿透、缓存击穿和缓存雪崩。这些问题不仅会影响系统的性能,还可能导致整个服务的瘫痪。
本文将深入分析这三种缓存问题的成因、危害以及对应的解决方案,通过实际代码示例和最佳实践,帮助开发者构建更加稳定、高效的缓存系统。
一、缓存穿透问题详解
1.1 什么是缓存穿透
缓存穿透是指查询一个根本不存在的数据,由于缓存中没有该数据,会直接访问数据库。如果数据库中也不存在该数据,就会导致请求绕过缓存,直接打到数据库上。
1.2 缓存穿透的危害
- 数据库压力增大:大量无效查询直接打到数据库
- 系统响应延迟:数据库查询耗时较长,影响整体性能
- 资源浪费:CPU、内存等系统资源被无效消耗
- 服务稳定性下降:可能导致数据库连接池耗尽
1.3 缓存穿透的典型场景
// 缓存穿透示例代码
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
// 1. 先从缓存中获取
String key = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
// 2. 缓存未命中,查询数据库
user = userMapper.selectById(id);
if (user == null) {
// 3. 数据库也不存在,直接返回null
return null;
} else {
// 4. 数据库存在,写入缓存
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
}
return user;
}
}
在上述代码中,当查询一个不存在的用户ID时,会经历以下流程:
- 缓存未命中
- 直接查询数据库
- 数据库查询结果为空
- 未将空值写入缓存
这样会导致每次查询不存在的数据都会访问数据库。
1.4 缓存穿透解决方案
方案一:布隆过滤器(Bloom Filter)
布隆过滤器是一种概率型数据结构,可以用来判断一个元素是否存在于集合中。通过在缓存层之前加入布隆过滤器,可以有效拦截不存在的数据请求。
@Component
public class BloomFilterService {
private static final int CAPACITY = 1000000;
private static final double ERROR_RATE = 0.01;
private BloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
// 初始化布隆过滤器
this.bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
CAPACITY,
ERROR_RATE
);
// 预热:将已存在的数据加入布隆过滤器
preheatBloomFilter();
}
public boolean contains(String key) {
return bloomFilter.mightContain(key);
}
public void add(String key) {
bloomFilter.put(key);
}
private void preheatBloomFilter() {
// 这里可以预热已存在的数据
List<String> existingKeys = getExistingKeys();
for (String key : existingKeys) {
bloomFilter.put(key);
}
}
private List<String> getExistingKeys() {
// 从数据库获取所有已存在的用户ID
return userMapper.selectAllIds();
}
}
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
@Autowired
private BloomFilterService bloomFilterService;
public User getUserById(Long id) {
String key = "user:" + id;
// 1. 先通过布隆过滤器判断是否存在
if (!bloomFilterService.contains(key)) {
return null; // 布隆过滤器判断不存在,直接返回
}
// 2. 缓存查询
User user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
// 3. 缓存未命中,查询数据库
user = userMapper.selectById(id);
if (user == null) {
// 4. 数据库也不存在,写入空值缓存
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return null;
} else {
// 5. 数据库存在,写入缓存
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
bloomFilterService.add(key); // 将存在的key加入布隆过滤器
}
}
return user;
}
}
方案二:空值缓存
将数据库查询结果为空的键也写入缓存,设置较短的过期时间。
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
String key = "user:" + id;
// 1. 先从缓存中获取
Object result = redisTemplate.opsForValue().get(key);
if (result == null) {
// 2. 缓存未命中,查询数据库
User user = userMapper.selectById(id);
if (user == null) {
// 3. 数据库不存在,缓存空值
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
return null;
} else {
// 4. 数据库存在,写入缓存
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
} else if ("".equals(result)) {
// 5. 缓存的是空值,直接返回null
return null;
}
return (User) result;
}
}
二、缓存击穿问题详解
2.1 什么是缓存击穿
缓存击穿是指某个热点数据在缓存中过期失效,此时大量并发请求同时访问该数据,导致这些请求都直接打到数据库上,造成数据库压力骤增。
2.2 缓存击穿的危害
- 数据库瞬时压力过大:大量并发请求同时访问数据库
- 服务响应时间延长:数据库查询耗时增加
- 系统可用性下降:可能导致数据库连接池耗尽
- 资源竞争激烈:CPU、内存等资源被大量占用
2.3 缓存击穿的典型场景
// 缓存击穿示例代码
@Service
public class ProductInfoService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
public Product getProductInfo(Long productId) {
String key = "product:" + productId;
// 1. 从缓存获取数据
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
// 2. 缓存过期,查询数据库
product = productMapper.selectById(productId);
if (product != null) {
// 3. 数据库存在,写入缓存
redisTemplate.opsForValue().set(key, product, 60, TimeUnit.SECONDS);
}
}
return product;
}
}
在高并发场景下,如果热点商品数据的缓存刚好过期,大量请求会同时访问数据库。
2.4 缓存击穿解决方案
方案一:互斥锁(Mutex Lock)
使用分布式锁来保证同一时间只有一个线程去查询数据库并更新缓存。
@Service
public class ProductInfoService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
public Product getProductInfo(Long productId) {
String key = "product:" + productId;
String lockKey = "lock:" + key;
// 1. 先从缓存获取数据
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
// 2. 缓存未命中,尝试获取分布式锁
String lockValue = UUID.randomUUID().toString();
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (acquired) {
try {
// 3. 获取锁成功,再次检查缓存
product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
// 4. 缓存仍然未命中,查询数据库
product = productMapper.selectById(productId);
if (product != null) {
// 5. 数据库存在,写入缓存
redisTemplate.opsForValue().set(key, product, 60, TimeUnit.SECONDS);
}
}
} finally {
// 6. 释放锁
releaseLock(lockKey, lockValue);
}
} else {
// 7. 获取锁失败,等待一段时间后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getProductInfo(productId); // 递归重试
}
}
return product;
}
private void releaseLock(String lockKey, String lockValue) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
}
方案二:设置热点数据永不过期
对于热点数据,可以设置永不过期策略,通过业务逻辑来更新缓存。
@Service
public class ProductInfoService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
// 热点商品ID列表
private static final Set<Long> HOT_PRODUCT_IDS = new HashSet<>();
static {
HOT_PRODUCT_IDS.add(1001L);
HOT_PRODUCT_IDS.add(1002L);
HOT_PRODUCT_IDS.add(1003L);
}
public Product getProductInfo(Long productId) {
String key = "product:" + productId;
// 1. 先从缓存获取数据
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
// 2. 检查是否为热点商品
if (HOT_PRODUCT_IDS.contains(productId)) {
// 3. 热点商品,使用永不过期策略
product = productMapper.selectById(productId);
if (product != null) {
// 4. 写入永不过期缓存
redisTemplate.opsForValue().set(key, product);
}
} else {
// 5. 非热点商品,使用普通过期策略
product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product, 60, TimeUnit.SECONDS);
}
}
}
return product;
}
// 定时任务更新热点商品缓存
@Scheduled(fixedRate = 30000) // 每30秒执行一次
public void updateHotProductCache() {
for (Long productId : HOT_PRODUCT_IDS) {
String key = "product:" + productId;
Product product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product);
}
}
}
}
三、缓存雪崩问题详解
3.1 什么是缓存雪崩
缓存雪崩是指缓存中大量数据同时过期,导致大量请求直接访问数据库,造成数据库压力骤增,甚至整个系统瘫痪的现象。
3.2 缓存雪崩的危害
- 系统整体性能下降:大量请求堆积在数据库层面
- 服务不可用:数据库连接池耗尽,服务响应超时
- 业务中断:核心功能无法正常提供服务
- 资源耗尽:CPU、内存、网络等资源被大量占用
3.3 缓存雪崩的典型场景
// 缓存雪崩示例代码
@Service
public class NewsService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private NewsMapper newsMapper;
public List<News> getHotNews() {
String key = "hot_news";
// 1. 从缓存获取热门新闻
List<News> newsList = (List<News>) redisTemplate.opsForValue().get(key);
if (newsList == null) {
// 2. 缓存未命中,查询数据库
newsList = newsMapper.selectHotNews();
if (newsList != null && !newsList.isEmpty()) {
// 3. 写入缓存(统一过期时间)
redisTemplate.opsForValue().set(key, newsList, 60, TimeUnit.SECONDS);
}
}
return newsList;
}
}
如果所有缓存数据都在同一时间过期,就会出现雪崩效应。
3.4 缓存雪崩解决方案
方案一:设置随机过期时间
为缓存数据设置随机的过期时间,避免大量数据同时过期。
@Service
public class NewsService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private NewsMapper newsMapper;
public List<News> getHotNews() {
String key = "hot_news";
// 1. 先从缓存获取数据
List<News> newsList = (List<News>) redisTemplate.opsForValue().get(key);
if (newsList == null) {
// 2. 缓存未命中,查询数据库
newsList = newsMapper.selectHotNews();
if (newsList != null && !newsList.isEmpty()) {
// 3. 设置随机过期时间(50-70秒)
int randomSeconds = 50 + new Random().nextInt(21);
redisTemplate.opsForValue().set(key, newsList, randomSeconds, TimeUnit.SECONDS);
}
}
return newsList;
}
}
方案二:多级缓存架构
构建多级缓存体系,包括本地缓存和分布式缓存。
@Component
public class MultiLevelCacheService {
// 本地缓存(Caffeine)
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.build();
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private NewsMapper newsMapper;
public List<News> getHotNews() {
String key = "hot_news";
// 1. 先从本地缓存获取
Object localResult = localCache.getIfPresent(key);
if (localResult != null) {
return (List<News>) localResult;
}
// 2. 从Redis缓存获取
List<News> redisResult = (List<News>) redisTemplate.opsForValue().get(key);
if (redisResult == null) {
// 3. Redis缓存未命中,查询数据库
redisResult = newsMapper.selectHotNews();
if (redisResult != null && !redisResult.isEmpty()) {
// 4. 写入Redis缓存(随机过期时间)
int randomSeconds = 50 + new Random().nextInt(21);
redisTemplate.opsForValue().set(key, redisResult, randomSeconds, TimeUnit.SECONDS);
// 5. 同时写入本地缓存
localCache.put(key, redisResult);
}
} else {
// 6. Redis缓存命中,更新本地缓存
localCache.put(key, redisResult);
}
return redisResult;
}
}
方案三:缓存预热和降级策略
通过缓存预热减少雪崩风险,并实现服务降级。
@Component
public class CacheWarmupService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private NewsMapper newsMapper;
@Autowired
private ProductMapper productMapper;
// 系统启动时预热缓存
@PostConstruct
public void warmupCache() {
// 预热热门新闻
warmupHotNews();
// 预热热门商品
warmupHotProducts();
// 启动定时任务,定期更新缓存
scheduleCacheUpdate();
}
private void warmupHotNews() {
List<News> newsList = newsMapper.selectHotNews();
if (newsList != null && !newsList.isEmpty()) {
String key = "hot_news";
// 设置较长时间的过期时间
redisTemplate.opsForValue().set(key, newsList, 120, TimeUnit.SECONDS);
}
}
private void warmupHotProducts() {
List<Product> productList = productMapper.selectHotProducts();
if (productList != null && !productList.isEmpty()) {
for (Product product : productList) {
String key = "product:" + product.getId();
// 设置较长时间的过期时间
redisTemplate.opsForValue().set(key, product, 120, TimeUnit.SECONDS);
}
}
}
private void scheduleCacheUpdate() {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// 定期更新热门新闻缓存
scheduler.scheduleAtFixedRate(() -> {
try {
List<News> newsList = newsMapper.selectHotNews();
if (newsList != null && !newsList.isEmpty()) {
String key = "hot_news";
redisTemplate.opsForValue().set(key, newsList, 120, TimeUnit.SECONDS);
}
} catch (Exception e) {
// 记录日志,但不影响主流程
log.error("Cache update failed", e);
}
}, 30, 60, TimeUnit.SECONDS);
}
}
四、综合优化策略
4.1 缓存策略最佳实践
@Component
public class ComprehensiveCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private BloomFilterService bloomFilterService;
// 缓存配置
private static final int DEFAULT_EXPIRE_TIME = 30; // 默认过期时间(秒)
private static final int HOT_DATA_EXPIRE_TIME = 3600; // 热点数据过期时间(秒)
private static final int MIN_RANDOM_EXPIRE = 50; // 随机过期最小值
private static final int MAX_RANDOM_EXPIRE = 70; // 随机过期最大值
public <T> T getData(String key, Class<T> clazz, Supplier<T> dataSupplier) {
// 1. 布隆过滤器检查
if (!bloomFilterService.contains(key)) {
return null;
}
// 2. 从缓存获取数据
Object cachedData = redisTemplate.opsForValue().get(key);
if (cachedData == null) {
// 3. 缓存未命中,使用分布式锁获取数据
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);
if (acquired) {
try {
// 再次检查缓存
cachedData = redisTemplate.opsForValue().get(key);
if (cachedData == null) {
// 4. 查询数据源
T data = dataSupplier.get();
if (data != null) {
// 5. 写入缓存(设置随机过期时间)
int expireTime = getRandomExpireTime();
redisTemplate.opsForValue().set(key, data, expireTime, TimeUnit.SECONDS);
bloomFilterService.add(key); // 加入布隆过滤器
} else {
// 6. 数据为空,写入空值缓存
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.SECONDS);
}
return data;
}
} finally {
releaseLock(lockKey, lockValue);
}
} else {
// 7. 获取锁失败,等待后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getData(key, clazz, dataSupplier);
}
} else if ("".equals(cachedData)) {
// 8. 空值缓存
return null;
}
return (T) cachedData;
}
private int getRandomExpireTime() {
return MIN_RANDOM_EXPIRE + new Random().nextInt(MAX_RANDOM_EXPIRE - MIN_RANDOM_EXPIRE + 1);
}
private void releaseLock(String lockKey, String lockValue) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
}
4.2 监控和告警机制
@Component
public class CacheMonitor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 缓存命中率监控
private final AtomicLong hitCount = new AtomicLong(0);
private final AtomicLong missCount = new AtomicLong(0);
public void recordCacheHit() {
hitCount.incrementAndGet();
}
public void recordCacheMiss() {
missCount.incrementAndGet();
}
public double getHitRate() {
long total = hitCount.get() + missCount.get();
if (total == 0) return 0.0;
return (double) hitCount.get() / total;
}
// 定期发送监控指标
@Scheduled(fixedRate = 60000)
public void sendMonitorMetrics() {
double hitRate = getHitRate();
if (hitRate < 0.8) { // 命中率低于80%时告警
log.warn("Cache hit rate is low: {}%", hitRate * 100);
// 发送告警通知
sendAlert("Cache hit rate warning", "Current hit rate: " + hitRate);
}
}
private void sendAlert(String title, String message) {
// 实现告警发送逻辑
log.info("Alert sent - Title: {}, Message: {}", title, message);
}
}
五、性能优化建议
5.1 Redis配置优化
# Redis连接池配置
redis.pool.maxTotal=200
redis.pool.maxIdle=50
redis.pool.minIdle=10
redis.pool.maxWaitMillis=1000
redis.pool.testOnBorrow=true
redis.pool.testOnReturn=true
# Redis超时设置
redis.timeout=2000
redis.database=0
# Redis序列化配置
redis.serializer.type=JDK
5.2 缓存键设计优化
public class CacheKeyGenerator {
// 统一的缓存键前缀
public static String buildKey(String prefix, Object... params) {
StringBuilder key = new StringBuilder(prefix);
for (Object param : params) {
key.append(":").append(param.toString());
}
return key.toString();
}
// 带版本号的缓存键
public static String buildVersionedKey(String prefix, String version, Object... params) {
StringBuilder key = new StringBuilder(prefix);
key.append(":v").append(version);
for (Object param : params) {
key.append(":").append(param.toString());
}
return key.toString();
}
}
结论
Redis缓存穿透、击穿、雪崩是高并发系统中常见的性能问题。通过本文的分析和解决方案,我们可以构建更加稳定、高效的缓存系统:
- 缓存穿透:使用布隆过滤器或空值缓存来拦截无效请求
- 缓存击穿:采用互斥锁或永不过期策略来保护热点数据
- 缓存雪崩:设置随机过期时间、多级缓存架构和缓存预热机制
在实际应用中,建议根据业务场景选择合适的解决方案,并结合监控告警机制,及时发现和处理潜在问题。通过合理的缓存策略设计和优化,可以显著提升系统的性能和稳定性,为用户提供更好的服务体验。
记住,在高并发场景下,缓存不仅仅是简单的数据存储,更是系统架构的重要组成部分。合理的设计和实现能够有效避免性能瓶颈,确保系统的高可用性。

评论 (0)