Redis挂了,你要如何处理应对?
面试场景
面试官:”如果你项目中的Redis挂了,整个系统会怎么样?你会怎么处理?”
这道题考察缓存故障处理能力。回答需要涵盖:
- Redis挂了会有什么影响
- 如何降级
- 如何恢复
Redis在系统中的角色
首先分析Redis在系统中承担什么职责:
| 场景 | 影响 | 严重程度 |
|---|---|---|
| 缓存 | 请求直接打到DB | 🔴 高 |
| Session存储 | 用户登录失效 | 🔴 高 |
| 分布式锁 | 并发控制失效 | 🔴 高 |
| 计数器/限流 | 限流失效 | 🟡 中 |
| 消息队列 | 消息丢失 | 🟡 中 |
故障场景分析
场景一:Redis完全不可用
所有Redis节点宕机,无法连接。
用户请求 → 应用 → Redis(超时)→ ???
↓
可能导致:
1. 请求超时
2. 线程池耗尽
3. 级联故障
场景二:Redis响应变慢
Redis未宕机,但响应延迟很高(如从5ms变成500ms)。
危害:比完全宕机更危险,会慢慢拖垮系统。
应对方案
方案一:本地缓存降级
当Redis不可用时,降级到本地缓存。
@Service
public class ProductService {
@Autowired
private Cache<Long, Product> localCache; // Caffeine
@Autowired
private RedisTemplate<String, Product> redisTemplate;
@Autowired
private CircuitBreaker redisCircuitBreaker;
public Product getProduct(Long productId) {
// L1: 本地缓存(始终先查)
Product product = localCache.getIfPresent(productId);
if (product != null) {
return product;
}
// L2: Redis(带熔断保护)
if (redisCircuitBreaker.allowRequest()) {
try {
product = redisTemplate.opsForValue().get("product:" + productId);
if (product != null) {
localCache.put(productId, product);
redisCircuitBreaker.recordSuccess();
return product;
}
} catch (Exception e) {
redisCircuitBreaker.recordFailure();
log.warn("Redis访问失败,降级到DB", e);
}
}
// L3: 数据库
product = productMapper.findById(productId);
if (product != null) {
localCache.put(productId, product);
// 注意:Redis故障期间不写入Redis
}
return product;
}
}
本地缓存配置
@Bean
public Cache<Long, Product> productLocalCache() {
return Caffeine.newBuilder()
.maximumSize(10000) // 最多1万条
.expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期
.build();
}
方案二:熔断保护
防止Redis故障导致线程池耗尽。
// 使用Sentinel实现熔断
@SentinelResource(
value = "getFromRedis",
fallback = "getFromRedisFallback",
blockHandler = "getFromRedisBlockHandler"
)
public Product getFromRedis(Long productId) {
return redisTemplate.opsForValue().get("product:" + productId);
}
// 降级逻辑
public Product getFromRedisFallback(Long productId, Throwable t) {
log.warn("Redis熔断,降级处理");
return productMapper.findById(productId);
}
熔断配置
# Sentinel规则
[
{
"resource": "getFromRedis",
"grade": 1, # 1=异常数, 0=慢调用比例
"count": 5, # 异常数阈值
"timeWindow": 10, # 熔断时长(秒)
"statIntervalMs": 10000 # 统计窗口
}
]
方案三:超时设置
避免Redis慢响应拖垮系统。
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofMillis(200)) // 命令超时200ms
.shutdownTimeout(Duration.ofMillis(100))
.build();
return new LettuceConnectionFactory(redisConfig(), clientConfig);
}
连接池配置
spring:
redis:
lettuce:
pool:
max-active: 50
max-idle: 20
min-idle: 5
max-wait: 100ms # 获取连接超时
方案四:分布式锁降级
Redis挂了,分布式锁失效,如何处理?
public class DistributedLockService {
@Autowired
private RedissonClient redisson;
@Autowired
private RateLimiter localRateLimiter; // 本地限流
public boolean tryLock(String key, long timeout, TimeUnit unit) {
try {
RLock lock = redisson.getLock(key);
return lock.tryLock(timeout, unit);
} catch (Exception e) {
log.warn("Redis分布式锁获取失败,降级为本地限流");
// 降级方案1: 本地限流(保证不超卖)
return localRateLimiter.tryAcquire();
}
}
}
更完善的方案:
- 备用数据库锁(ZooKeeper、MySQL)
- 接受短时间内的部分超卖,事后补偿
方案五:Session降级
@Component
public class SessionManager {
@Autowired
private RedisTemplate<String, UserSession> redisTemplate;
// 本地Session缓存(降级用)
private Cache<String, UserSession> localSessionCache = Caffeine.newBuilder()
.maximumSize(50000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();
public UserSession getSession(String token) {
// 优先Redis
try {
UserSession session = redisTemplate.opsForValue().get("session:" + token);
if (session != null) {
localSessionCache.put(token, session); // 同步到本地
return session;
}
} catch (Exception e) {
log.warn("Redis不可用,使用本地Session");
}
// 降级到本地缓存
return localSessionCache.getIfPresent(token);
}
}
恢复后的处理
1. 缓存预热
Redis恢复后,需要预热,避免冷启动导致DB压力过大。
@Component
public class CacheWarmer {
@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
// 1. 热点商品预热
List<Long> hotProductIds = getHotProductIds();
for (Long id : hotProductIds) {
Product product = productMapper.findById(id);
redisTemplate.opsForValue().set("product:" + id, product);
}
// 2. 控制预热速度,避免瞬间压力
Thread.sleep(10); // 每条休息10ms
}
}
2. 流量灰度
恢复后流量控制:
1. 先放10%流量到Redis
2. 观察Redis负载,无问题后逐步放量
3. 完全切换后关闭降级开关
3. 数据一致性修复
降级期间可能产生数据不一致:
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点
public void syncCacheWithDB() {
// 全量对比DB和Redis数据
// 发现不一致则以DB为准刷新Redis
}
监控告警
# Redis监控指标
- redis_connected_clients # 连接数
- redis_memory_used_bytes # 内存使用
- redis_ops_per_sec # OPS
- redis_slow_log # 慢查询
# 告警规则
- alert: RedisDown
expr: redis_up == 0
for: 30s
labels:
severity: P0
- alert: RedisHighLatency
expr: redis_commands_duration_seconds_sum > 0.1
for: 1m
labels:
severity: P1
面试答题框架
影响分析:
- 缓存失效:DB压力剧增
- Session失效:用户掉线
- 分布式锁失效:并发问题
应对方案:
1. 本地缓存降级(Caffeine兜底)
2. 熔断保护(Sentinel/Hystrix)
3. 超时设置(避免线程阻塞)
4. 分布式锁降级(本地限流/备用锁)
恢复处理:
1. 缓存预热
2. 流量灰度
3. 数据一致性修复
预防措施:
- Redis集群/哨兵高可用
- 完善监控告警
总结
| 场景 | 降级方案 | 注意事项 |
|---|---|---|
| 缓存 | 本地缓存 | 容量限制、过期时间 |
| Session | 本地缓存 | 多实例数据不同步 |
| 分布式锁 | 本地限流 | 可能短时超卖 |
| 限流 | 本地限流 | 精度下降 |