一、高频面试题
- 什么是缓存雪崩?它和缓存穿透、缓存击穿的区别是什么?
缓存雪崩是指在某一时间段,缓存大量失效,大量请求直接落到数据库上,导致数据库负载过高甚至宕机,进而使整个系统崩溃。缓存穿透是指查询一个一定不存在的数据,每次请求都直接打到数据库;缓存击穿是指一个热点 key,在缓存失效的瞬间,大量请求同时访问该 key,导致数据库压力骤增。三者本质上都是缓存失效引发的问题,但失效场景和影响范围有所不同。 - 缓存雪崩可能产生的原因有哪些?
- 集中失效:缓存中的大量 key 设置了相同或相近的过期时间,导致在同一时刻大量缓存失效。
- 缓存服务故障:如 Redis 集群部分节点宕机,导致大量缓存无法访问。
- 高并发流量冲击:短时间内突然涌入大量请求,即使缓存正常,也可能因缓存无法及时响应而使请求大量落到数据库。
- 如何应对缓存雪崩?
常见的应对方案包括:设置不同的缓存过期时间避免集中失效;采用多级缓存,降低对单一缓存的依赖;使用熔断降级机制,在缓存故障时快速返回默认值或友好提示;加强缓存服务的高可用性,如搭建 Redis 主从集群、哨兵模式等。
二、案例分析
在电商系统中,缓存的使用是提升系统性能和吞吐量的重要手段。然而,缓存雪崩故障一旦发生,会对系统造成严重冲击,导致大量请求直接压到数据库,引发系统响应缓慢甚至崩溃。下面,我将结合实际遇到的电商系统缓存雪崩故障案例,为大家详细解析故障排查与解决的全流程。
1、故障现象
某电商平台在一次大促活动期间,系统突然出现响应时间急剧增加的情况。原本响应时间在几十毫秒的接口,平均响应时间飙升至数秒,部分接口甚至超时无响应。通过监控系统发现,数据库的负载急剧升高,CPU 使用率达到 100%,数据库连接池被耗尽,大量请求处于等待状态。同时,缓存服务(Redis)的 QPS(每秒查询率)大幅下降,很多请求无法从缓存中获取数据,转而请求数据库。用户端表现为页面加载缓慢,商品详情页无法显示,下单操作长时间无响应,导致大量用户流失和投诉。
2、故障分析
2.1 初步判断
根据故障现象,初步怀疑是缓存雪崩导致的问题。缓存雪崩是指由于缓存层大面积失效,大量请求直接落到数据库,造成数据库压力过大甚至崩溃的情况。可能导致缓存雪崩的原因有多种,比如缓存服务宕机、大量缓存集中过期、缓存穿透引发数据库压力过大进而影响缓存服务等。
2.2 深入排查
- 检查缓存服务状态:通过运维监控平台查看Redis的运行状态,发现Redis服务正常运行,没有出现宕机、主从切换等异常情况,排除了缓存服务本身故障导致雪崩的可能。
- 分析缓存数据过期情况:查看缓存数据的过期时间设置,发现为了应对大促活动,提前将大量商品数据的缓存过期时间设置为活动开始后的同一时间点。在活动开始瞬间,这些缓存同时失效,大量请求无法命中缓存,直接访问数据库,这很可能是导致缓存雪崩的主要原因。
- 排查缓存穿透:通过日志分析,没有发现大量请求访问不存在的缓存数据的情况,初步排除了缓存穿透导致雪崩的可能性。
3、故障定位
通过上述分析,基本确定此次缓存雪崩故障是由于大量缓存集中过期导致的。在大促活动场景下,为了保证商品数据的实时性,设置了统一的缓存过期时间,但没有考虑到这种设置会带来的集中失效问题。当缓存过期后,所有针对这些商品数据的请求都会直接打到数据库,而数据库无法承受如此大量的并发请求,导致性能急剧下降,进而影响整个系统的可用性。
4、解决方法
4.1 分散缓存过期时间
为了避免缓存集中过期,对缓存过期时间的设置进行优化。在设置缓存时,给每个缓存数据的过期时间添加一个随机的时间偏移量,使缓存过期时间分散开来。以下是使用Java和Jedis操作Redis实现分散缓存过期时间的代码示例:
java
import redis.clients.jedis.Jedis;
public class CacheUtils {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
// 设置缓存并分散过期时间
public static void setWithRandomExpire(String key, String value, int baseExpireSeconds, int randomRangeSeconds) {
try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {
// 生成随机的过期时间偏移量
int randomOffset = (int) (Math.random() * randomRangeSeconds);
int actualExpireSeconds = baseExpireSeconds + randomOffset;
jedis.setex(key, actualExpireSeconds, value);
}
}
// 获取缓存
public static String get(String key) {
try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {
return jedis.get(key);
}
}
}
使用示例:
java
public class Main {
public static void main(String[] args) {
// 假设基础过期时间为600秒,随机范围为120秒
CacheUtils.setWithRandomExpire("product:123", "product details", 600, 120);
String value = CacheUtils.get("product:123");
System.out.println(value);
}
}
通过上述代码,在设置缓存时,会根据传入的基础过期时间和随机范围,生成一个随机的实际过期时间,使得缓存过期时间不再集中在同一时刻。
4.2 使用缓存预热
在大促活动开始前,提前将热门商品数据加载到缓存中,避免活动开始时大量请求同时查询数据库。可以通过定时任务或者手动触发的方式进行缓存预热。以下是使用Java和Jedis实现缓存预热的代码示例:
java
import redis.clients.jedis.Jedis;
import java.util.List;
public class CachePreheating {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
// 假设从数据库获取商品数据的方法
public static List<String> getProductDataFromDatabase() {
// 这里模拟从数据库获取数据,实际应替换为真实的数据库查询逻辑
return List.of("product1 data", "product2 data", "product3 data");
}
public static void preheatCache() {
try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {
List<String> productDataList = getProductDataFromDatabase();
for (int i = 0; i < productDataList.size(); i++) {
String key = "product:" + (i + 1);
String value = productDataList.get(i);
// 设置一个较长的过期时间,比如一天
jedis.setex(key, 24 * 60 * 60, value);
}
}
}
}
使用示例:
java
public class Main {
public static void main(String[] args) {
CachePreheating.preheatCache();
System.out.println("缓存预热完成");
}
}
在实际应用中,getProductDataFromDatabase
方法应替换为真实的从数据库查询商品数据的逻辑,通过这种方式在活动开始前将数据提前加载到缓存中,减少活动开始时数据库的压力。
4.3 构建多级缓存
为了进一步降低数据库的压力,可以构建多级缓存。除了Redis作为一级缓存外,还可以在应用服务器本地设置二级缓存,例如使用Guava Cache。当请求到达时,先查询应用服务器本地的二级缓存,如果未命中再查询Redis一级缓存,最后才查询数据库。以下是使用Guava Cache实现二级缓存的代码示例:
java
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import redis.clients.jedis.Jedis;
import java.util.concurrent.TimeUnit;
public class MultiLevelCache {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
// 构建Guava Cache作为二级缓存
private static final Cache<String, String> localCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(60, TimeUnit.MINUTES)
.build();
// 从缓存获取数据
public static String get(String key) {
String value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {
value = jedis.get(key);
if (value != null) {
localCache.put(key, value);
}
return value;
}
}
// 设置缓存数据
public static void set(String key, String value, int expireSeconds) {
try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {
jedis.setex(key, expireSeconds, value);
localCache.put(key, value);
}
}
}
使用示例:
java
public class Main {
public static void main(String[] args) {
MultiLevelCache.set("product:456", "new product details", 3600);
String value = MultiLevelCache.get("product:456");
System.out.println(value);
}
}
通过多级缓存的设置,大部分请求可以在本地缓存或者Redis缓存中得到响应,减少了对数据库的访问次数,提高了系统的整体性能和稳定性。
4.4 数据库连接池优化
为了应对突发的高并发请求,对数据库连接池进行优化。调整连接池的最大连接数、最小空闲连接数等参数,确保数据库能够合理地处理请求。以HikariCP连接池为例,以下是优化后的配置示例:
java
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
public class DatabaseConfig {
private static final String DB_URL = "jdbc:mysql://localhost:3306/ecommerce";
private static final String DB_USER = "root";
private static final String DB_PASSWORD = "password";
public static DataSource getDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(DB_URL);
config.setUsername(DB_USER);
config.setPassword(DB_PASSWORD);
// 最大连接数,根据数据库服务器性能和业务需求调整
config.setMaximumPoolSize(100);
// 最小空闲连接数
config.setMinimumIdle(10);
// 连接超时时间
config.setConnectionTimeout(30000);
// 空闲连接存活最大时间
config.setIdleTimeout(600000);
// 连接的最大生命周期
config.setMaxLifetime(1800000);
return new HikariDataSource(config);
}
}
通过合理配置数据库连接池参数,能够提高数据库处理请求的能力,避免因连接池耗尽导致请求积压和系统崩溃。
5、预防策略
5.1 监控与告警
建立完善的缓存和数据库监控体系,实时监控缓存的命中率、QPS、内存使用情况,以及数据库的负载、连接数、慢查询等指标。当这些指标超过预设阈值时,及时发出告警通知运维和开发人员。例如,使用Prometheus和Grafana搭建监控平台,对Redis和数据库进行监控,并配置邮件、短信等告警方式。
5.2 容量规划
根据业务增长情况和历史数据,对缓存和数据库进行容量规划。提前评估大促活动等高峰期的流量和数据量,合理规划缓存和数据库的资源,避免因资源不足导致故障。同时,定期对缓存和数据库进行性能测试和优化,确保系统能够稳定运行。
5.3 应急预案制定
制定详细的缓存雪崩应急预案,明确在故障发生时的处理流程和责任人。定期对应急预案进行演练,确保相关人员熟悉处理步骤,能够在故障发生时快速响应和处理,将损失降到最低。
通过以上对电商系统缓存雪崩故障的排查、解决和预防策略的详细解析,希望能够帮助大家在实际工作中更好地应对类似问题,构建稳定可靠的电商系统。在实际开发和运维过程中,还需要根据具体业务场景和系统架构,不断优化和完善缓存和数据库的设计与管理,提高系统的整体性能和可用性。