一、Bug 场景
在一个电商系统中,Redis 被广泛用于缓存商品信息、用户信息等各种数据。为了管理缓存,系统设置了不同数据的缓存过期时间。在某个特定时间点,大量缓存同时过期,导致大量请求直接涌向数据库,数据库不堪重负,最终可能导致整个系统崩溃,这就是典型的缓存雪崩场景。例如,在一次促销活动后,大量商品的缓存同时到期,大量用户在活动结束后查询商品信息,从而引发问题。
二、代码示例
商品服务(有缺陷)
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 假设这是数据库查询方法
private Object queryProductFromDB(String productId) {
// 模拟数据库查询逻辑,返回商品信息
return "Product Data";
}
public Object getProduct(String productId) {
Object product = redisTemplate.opsForValue().get(productId);
if (product == null) {
product = queryProductFromDB(productId);
if (product != null) {
// 设置缓存,假设过期时间为1小时,且大量商品都设置相同过期时间
redisTemplate.opsForValue().set(productId, product, 1, TimeUnit.HOURS);
}
}
return product;
}
}
三、问题描述
- 预期行为:缓存过期时,请求能均匀地分散到数据库,数据库可以承受这些请求,系统稳定运行。
- 实际行为:大量缓存同时过期,大量请求瞬间穿透缓存到达数据库,数据库无法承受如此高的并发压力,可能出现响应缓慢、服务中断甚至崩溃的情况。这是因为在设计缓存过期时间时,没有充分考虑到大量缓存同时过期带来的风险,导致在同一时刻大量请求失去缓存的保护直接访问数据库。
四、解决方案
- 随机过期时间:为每个缓存设置一个随机的过期时间,避免大量缓存集中过期。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 假设这是数据库查询方法
private Object queryProductFromDB(String productId) {
// 模拟数据库查询逻辑,返回商品信息
return "Product Data";
}
public Object getProduct(String productId) {
Object product = redisTemplate.opsForValue().get(productId);
if (product == null) {
product = queryProductFromDB(productId);
if (product != null) {
// 设置随机过期时间,范围在1小时到1.5小时之间
int randomExpiration = ThreadLocalRandom.current().nextInt(3600, 5400);
redisTemplate.opsForValue().set(productId, product, randomExpiration, TimeUnit.SECONDS);
}
}
return product;
}
}
- 加锁排队:当缓存失效时,通过加锁机制确保只有部分请求去查询数据库并更新缓存,其他请求等待,这样可以防止大量请求同时冲击数据库。
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 假设这是数据库查询方法
private Object queryProductFromDB(String productId) {
// 模拟数据库查询逻辑,返回商品信息
return "Product Data";
}
public Object getProduct(String productId) {
Object product = redisTemplate.opsForValue().get(productId);
if (product == null) {
String lockKey = "product:lock:" + productId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 1, TimeUnit.MINUTES);
if (locked) {
try {
product = queryProductFromDB(productId);
if (product != null) {
redisTemplate.opsForValue().set(productId, product, 1, TimeUnit.HOURS);
}
} finally {
redisTemplate.delete(lockKey);
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getProduct(productId);
}
}
return product;
}
}
- 二级缓存:采用二级缓存架构,一级缓存使用 Redis,二级缓存可以使用本地缓存(如 Caffeine)。当一级缓存失效时,先从二级缓存获取数据,如果二级缓存也没有,则查询数据库,并将数据同时放入一级和二级缓存。
java
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final Cache<String, Object> localCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
// 假设这是数据库查询方法
private Object queryProductFromDB(String productId) {
// 模拟数据库查询逻辑,返回商品信息
return "Product Data";
}
public Object getProduct(String productId) {
Object product = localCache.getIfPresent(productId);
if (product == null) {
product = redisTemplate.opsForValue().get(productId);
if (product == null) {
product = queryProductFromDB(productId);
if (product != null) {
redisTemplate.opsForValue().set(productId, product, 1, TimeUnit.HOURS);
localCache.put(productId, product);
}
} else {
localCache.put(productId, product);
}
}
return product;
}
}