一、高频面试题
面试题1:请说说什么是缓存雪崩,它产生的原因主要有哪些?
参考答案:缓存雪崩就是大量缓存数据在同一时间失效,或者缓存服务突然挂掉,导致大量原本该走缓存的请求,一股脑全打到数据库上,数据库扛不住压力就可能崩溃。原因主要有两种,一是缓存设置不合理,比如很多缓存设置了相同或相近的过期时间,到期时同时失效;二是缓存服务故障,像 Redis 因为内存满了、网络断了或者自身 bug 导致无法提供服务。
第一步追问:如果是缓存过期时间设置导致雪崩,常见的错误设置方式有哪些?
参考答案:最常见的就是给所有缓存都设成一样的过期时间,比如把所有商品信息缓存都设为 1 小时过期,时间一到就集体失效。还有就是没有考虑业务实际情况,对更新频繁的数据设置过长过期时间,数据变了缓存却没更新,最后大量过期时引发雪崩。
第二步追问:缓存服务故障引发雪崩,一般会是哪些故障情况?
参考答案:比如 Redis 服务器内存耗尽,无法再接收新的写请求;网络不稳定,客户端和 Redis 之间频繁断连;Redis 集群中主节点宕机,且故障转移不及时;或者 Redis 本身存在程序 bug,运行过程中突然崩溃。
第三步追问:怎么区分雪崩是因为缓存过期还是缓存服务故障导致的?
参考答案:看监控数据,如果 Redis 服务状态正常,但是命中率突然暴跌,数据库请求量暴增,那大概率是缓存过期导致;要是 Redis 直接连接不上,或者响应超时,那很可能是缓存服务本身出故障了。
面试题2:发现缓存雪崩后,怎么去排查和定位问题?
参考答案:先看系统监控数据,确认是不是缓存雪崩,比如看 Redis 命中率是不是突然降低,数据库 QPS 是不是飙升。然后检查缓存过期时间配置,看有没有大量缓存集中过期。接着查看缓存服务运行状态,有没有报错日志,资源使用情况是否正常,比如内存、CPU 使用率。最后分析业务流量变化,是不是有突发流量导致缓存扛不住。
第一步追问:怎么快速查看缓存过期时间配置是否有问题?
参考答案:可以通过 Redis 的管理工具,像 Redis - CLI 去查看 key 的过期时间,看看有没有大量 key 的过期时间集中在某个时间段。也可以结合业务代码,检查设置过期时间的逻辑,看是不是有不合理的地方。
第二步追问:缓存服务资源使用情况主要关注哪些指标?
参考答案:重点关注内存使用率,要是快满了或者已经满了,就容易出问题;CPU 使用率也得看,如果持续过高,可能影响 Redis 处理请求的效率;还有网络带宽占用,网络堵了会导致请求超时。
第三步追问:如果排查出是业务突发流量导致雪崩,后续该怎么处理?
参考答案:先做限流,把请求量控制在系统能承受的范围内;再紧急扩容缓存资源,比如增加 Redis 节点;最后优化缓存策略,对热点数据做特殊处理,避免下次再出现类似问题。
二、案例分析
在电商系统中,缓存的使用是提升系统性能和吞吐量的重要手段。然而,缓存雪崩故障一旦发生,会对系统造成严重冲击,导致大量请求直接压到数据库,引发系统响应缓慢甚至崩溃。下面,我将结合实际遇到的电商系统缓存雪崩故障案例,为大家详细解析故障排查与解决的全流程。
1、故障现象
某电商平台在一次大促活动期间,系统突然出现响应时间急剧增加的情况。原本响应时间在几十毫秒的接口,平均响应时间飙升至数秒,部分接口甚至超时无响应。通过监控系统发现,数据库的负载急剧升高,CPU 使用率达到 100%,数据库连接池被耗尽,大量请求处于等待状态。同时,缓存服务(Redis)的 QPS(每秒查询率)大幅下降,很多请求无法从缓存中获取数据,转而请求数据库。用户端表现为页面加载缓慢,商品详情页无法显示,下单操作长时间无响应,导致大量用户流失和投诉。
2、故障分析
2.1 初步判断
根据故障现象,初步怀疑是缓存雪崩导致的问题。缓存雪崩是指由于缓存层大面积失效,大量请求直接落到数据库,造成数据库压力过大甚至崩溃的情况。可能导致缓存雪崩的原因有多种,比如缓存服务宕机、大量缓存集中过期、缓存穿透引发数据库压力过大进而影响缓存服务等。
2.2 深入排查
- 检查缓存服务状态:通过运维监控平台查看Redis的运行状态,发现Redis服务正常运行,没有出现宕机、主从切换等异常情况,排除了缓存服务本身故障导致雪崩的可能。
- 分析缓存数据过期情况:查看缓存数据的过期时间设置,发现为了应对大促活动,提前将大量商品数据的缓存过期时间设置为活动开始后的同一时间点。在活动开始瞬间,这些缓存同时失效,大量请求无法命中缓存,直接访问数据库,这很可能是导致缓存雪崩的主要原因。
- 排查缓存穿透:通过日志分析,没有发现大量请求访问不存在的缓存数据的情况,初步排除了缓存穿透导致雪崩的可能性。
3、故障定位
通过上述分析,基本确定此次缓存雪崩故障是由于大量缓存集中过期导致的。在大促活动场景下,为了保证商品数据的实时性,设置了统一的缓存过期时间,但没有考虑到这种设置会带来的集中失效问题。当缓存过期后,所有针对这些商品数据的请求都会直接打到数据库,而数据库无法承受如此大量的并发请求,导致性能急剧下降,进而影响整个系统的可用性。
4、解决方法
4.1 分散缓存过期时间
为了避免缓存集中过期,对缓存过期时间的设置进行优化。在设置缓存时,给每个缓存数据的过期时间添加一个随机的时间偏移量,使缓存过期时间分散开来。以下是使用Java和Jedis操作Redis实现分散缓存过期时间的代码示例:
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);
}
}
}
使用示例:
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实现缓存预热的代码示例:
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);
}
}
}
}
使用示例:
public class Main {
public static void main(String[] args) {
CachePreheating.preheatCache();
System.out.println("缓存预热完成");
}
}
在实际应用中,getProductDataFromDatabase
方法应替换为真实的从数据库查询商品数据的逻辑,通过这种方式在活动开始前将数据提前加载到缓存中,减少活动开始时数据库的压力。
4.3 构建多级缓存
为了进一步降低数据库的压力,可以构建多级缓存。除了Redis作为一级缓存外,还可以在应用服务器本地设置二级缓存,例如使用Guava Cache。当请求到达时,先查询应用服务器本地的二级缓存,如果未命中再查询Redis一级缓存,最后才查询数据库。以下是使用Guava Cache实现二级缓存的代码示例:
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);
}
}
}
使用示例:
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连接池为例,以下是优化后的配置示例:
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 应急预案制定
制定详细的缓存雪崩应急预案,明确在故障发生时的处理流程和责任人。定期对应急预案进行演练,确保相关人员熟悉处理步骤,能够在故障发生时快速响应和处理,将损失降到最低。
通过以上对电商系统缓存雪崩故障的排查、解决和预防策略的详细解析,希望能够帮助大家在实际工作中更好地应对类似问题,构建稳定可靠的电商系统。在实际开发和运维过程中,还需要根据具体业务场景和系统架构,不断优化和完善缓存和数据库的设计与管理,提高系统的整体性能和可用性。