缓存设计详解
一、知识概述
缓存是提升系统性能的重要手段,但不合理的缓存设计可能导致缓存穿透、缓存雪崩、缓存击穿等问题,甚至引发系统故障。本文将详细介绍这些问题的原理和解决方案,帮助设计健壮的缓存系统。
二、缓存穿透
2.1 问题介绍
缓存穿透是指查询一个根本不存在的数据,由于缓存中没有数据,每次都会查询数据库,导致缓存失去了保护数据库的作用。
场景举例:
- 恶意请求查询不存在的用户 ID
- 查询不存在的商品信息
- 查询不存在的订单号
危害:
- 数据库压力骤增
- 可能导致数据库宕机
- 浪费系统资源
2.2 解决方案
方案一:缓存空值
当查询数据库没有结果时,将空值写入缓存,并设置较短的过期时间。
// 缓存空值解决方案
public class CacheNullValue {
private Jedis jedis;
public String getData(String key) {
// 1. 查询缓存
String value = jedis.get(key);
// 2. 缓存存在,直接返回
if (value != null) {
// 判断是否为空值标记
if ("NULL".equals(value)) {
return null;
}
return value;
}
// 3. 查询数据库
value = queryFromDB(key);
// 4. 写入缓存
if (value != null) {
jedis.setex(key, 3600, value);
} else {
// 缓存空值,过期时间较短
jedis.setex(key, 60, "NULL");
}
return value;
}
private String queryFromDB(String key) {
// 模拟数据库查询
return null;
}
}
优点:实现简单,有效防止穿透 缺点:
- 占用缓存内存
- 可能缓存大量无效 key
- 数据一致性窗口期
方案二:布隆过滤器
布隆过滤器是一种空间效率很高的数据结构,用于判断元素是否在集合中。
// 布隆过滤器实现(简化版)
public class BloomFilter {
private BitSet bitSet;
private int size;
private int[] hashSeeds;
public BloomFilter(int size, int hashCount) {
this.size = size;
this.bitSet = new BitSet(size);
this.hashSeeds = new int[hashCount];
Random random = new Random();
for (int i = 0; i < hashCount; i++) {
hashSeeds[i] = random.nextInt();
}
}
// 添加元素
public void add(String value) {
for (int seed : hashSeeds) {
int hash = hash(value, seed);
bitSet.set(hash);
}
}
// 判断元素是否存在
public boolean mightContain(String value) {
for (int seed : hashSeeds) {
int hash = hash(value, seed);
if (!bitSet.get(hash)) {
return false;
}
}
return true;
}
private int hash(String value, int seed) {
int hash = 0;
for (char c : value.toCharArray()) {
hash = hash * seed + c;
}
return Math.abs(hash) % size;
}
}
// 使用布隆过滤器防止穿透
public class CacheWithBloomFilter {
private Jedis jedis;
private BloomFilter bloomFilter;
public String getData(String key) {
// 1. 先检查布隆过滤器
if (!bloomFilter.mightContain(key)) {
// 确定不存在,直接返回
return null;
}
// 2. 查询缓存
String value = jedis.get(key);
if (value != null) {
return "NULL".equals(value) ? null : value;
}
// 3. 查询数据库
value = queryFromDB(key);
// 4. 写入缓存
if (value != null) {
jedis.setex(key, 3600, value);
} else {
// 可能是误判,仍然缓存空值
jedis.setex(key, 60, "NULL");
}
return value;
}
// 初始化布隆过滤器
public void initBloomFilter() {
// 从数据库加载所有有效 key
List<String> allKeys = loadAllKeysFromDB();
for (String key : allKeys) {
bloomFilter.add(key);
}
}
private List<String> loadAllKeysFromDB() {
// 实现从数据库加载
return new ArrayList<>();
}
private String queryFromDB(String key) {
return null;
}
}
使用 Redis 实现 Bloom Filter:
// Redis 布隆过滤器
public class RedisBloomFilter {
private Jedis jedis;
private String key;
private int size;
private int hashCount;
public RedisBloomFilter(Jedis jedis, String key, int size, int hashCount) {
this.jedis = jedis;
this.key = key;
this.size = size;
this.hashCount = hashCount;
}
// 添加元素
public void add(String value) {
for (int i = 0; i < hashCount; i++) {
int hash = hash(value, i);
jedis.setbit(key, hash, true);
}
}
// 判断是否存在
public boolean mightContain(String value) {
for (int i = 0; i < hashCount; i++) {
int hash = hash(value, i);
if (!jedis.getbit(key, hash)) {
return false;
}
}
return true;
}
// 使用 MurmurHash(更好哈希算法)
private int hash(String value, int seed) {
// 简化实现,生产环境建议使用 Guava 的 BloomFilter
return Math.abs((value.hashCode() * (seed + 1)) % size);
}
}
// 使用 Guava BloomFilter
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class GuavaBloomFilterDemo {
public static void main(String[] args) {
// 创建布隆过滤器
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 预期元素数量
0.01 // 误判率
);
// 添加元素
filter.put("user:1001");
filter.put("user:1002");
// 判断是否存在
System.out.println(filter.mightContain("user:1001")); // true
System.out.println(filter.mightContain("user:9999")); // false(大概率)
}
}
优点:
- 空间效率高
- 查询速度快
- 不存在误判不存在的情况
缺点:
- 存在误判(判断存在但实际不存在)
- 不支持删除
- 需要预加载数据
方案三:参数校验
在请求到达缓存之前,先进行参数合法性校验。
// 参数校验拦截
public class RequestValidator {
public boolean isValidRequest(String key) {
// 1. 基础校验
if (key == null || key.isEmpty()) {
return false;
}
// 2. 格式校验
if (!key.matches("^user:\\d+$")) {
return false;
}
// 3. 范围校验
String idStr = key.substring(5);
try {
long id = Long.parseLong(idStr);
if (id < 1 || id > 1000000000L) {
return false;
}
} catch (NumberFormatException e) {
return false;
}
return true;
}
}
// 在 Controller 层使用
@RestController
public class UserController {
@Autowired
private RequestValidator validator;
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public User getUser(@PathVariable String id) {
String key = "user:" + id;
// 参数校验
if (!validator.isValidRequest(key)) {
throw new IllegalArgumentException("Invalid user id");
}
return userService.getUser(key);
}
}
2.3 综合解决方案
// 完整的防穿透方案
public class AntiPenetrationCache {
private Jedis jedis;
private BloomFilter<String> bloomFilter;
private static final String NULL_VALUE = "##NULL##";
public String getData(String key) {
// 1. 参数校验
if (!isValidKey(key)) {
return null;
}
// 2. 布隆过滤器检查
if (!bloomFilter.mightContain(key)) {
return null;
}
// 3. 缓存查询
String value = jedis.get(key);
if (value != null) {
return NULL_VALUE.equals(value) ? null : value;
}
// 4. 数据库查询
value = queryFromDB(key);
// 5. 写入缓存
if (value != null) {
jedis.setex(key, getRandomExpireTime(), value);
} else {
// 缓存空值,短过期时间
jedis.setex(key, 60, NULL_VALUE);
}
return value;
}
// 随机过期时间,防止同时过期
private int getRandomExpireTime() {
return 3600 + new Random().nextInt(600);
}
private boolean isValidKey(String key) {
return key != null && key.matches("^\\w+:\\d+$");
}
private String queryFromDB(String key) {
return null;
}
}
三、缓存雪崩
3.1 问题介绍
缓存雪崩是指大量缓存同时失效或Redis 服务宕机,导致所有请求直接打到数据库,造成数据库压力骤增甚至宕机。
场景举例:
- 大量 key 设置了相同的过期时间
- Redis 服务重启或宕机
- 缓存预热时间集中
危害:
- 数据库瞬间压力巨大
- 系统响应变慢甚至不可用
- 连锁反应导致整个系统崩溃
3.2 解决方案
方案一:过期时间加随机值
在设置缓存过期时间时,添加随机值,避免同时过期。
// 随机过期时间
public class RandomExpireCache {
private Jedis jedis;
// 基础过期时间(秒)
private int baseExpireTime = 3600;
// 随机范围(秒)
private int randomRange = 600;
public void set(String key, String value) {
int expireTime = baseExpireTime + new Random().nextInt(randomRange);
jedis.setex(key, expireTime, value);
}
public void batchSet(Map<String, String> data) {
for (Map.Entry<String, String> entry : data.entrySet()) {
set(entry.getKey(), entry.getValue());
}
}
}
方案二:多级缓存
使用多级缓存,即使一级缓存失效,二级缓存仍可提供数据。
// 多级缓存实现
public class MultiLevelCache {
// 一级缓存:本地缓存(Caffeine)
private Cache<String, String> localCache;
// 二级缓存:Redis
private Jedis jedis;
public MultiLevelCache() {
// 本地缓存配置
localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(300, TimeUnit.SECONDS) // 5分钟过期
.build();
}
public String get(String key) {
// 1. 查询一级缓存(本地)
String value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 查询二级缓存(Redis)
value = jedis.get(key);
if (value != null) {
// 回填一级缓存
localCache.put(key, value);
return value;
}
// 3. 查询数据库
value = queryFromDB(key);
if (value != null) {
// 写入多级缓存
jedis.setex(key, 3600, value);
localCache.put(key, value);
}
return value;
}
// 使用分布式锁防止缓存重建时的并发问题
public String getWithLock(String key) {
// 1. 查询本地缓存
String value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 查询 Redis
value = jedis.get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// 3. 获取分布式锁,防止重复查询数据库
String lockKey = "lock:" + key;
try {
if (tryLock(lockKey, 10)) {
// 双重检查
value = jedis.get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// 查询数据库
value = queryFromDB(key);
// 写入缓存
if (value != null) {
jedis.setex(key, 3600, value);
localCache.put(key, value);
}
} else {
// 获取锁失败,等待后重试
Thread.sleep(50);
return getWithLock(key);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
unlock(lockKey);
}
return value;
}
private boolean tryLock(String key, int expireSeconds) {
return "OK".equals(jedis.set(key, "1", "NX", "EX", expireSeconds));
}
private void unlock(String key) {
jedis.del(key);
}
private String queryFromDB(String key) {
return "data:" + key;
}
}
方案三:熔断降级
当缓存失效导致数据库压力过大时,触发熔断,保护系统。
// 熔断降级实现(使用 Resilience4j)
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
public class CircuitBreakerCache {
private Jedis jedis;
private CircuitBreaker circuitBreaker;
public CircuitBreakerCache() {
// 熔断器配置
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率阈值 50%
.waitDurationInOpenState(Duration.ofSeconds(30)) // 开启状态等待时间
.slidingWindowSize(100) // 滑动窗口大小
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.build();
circuitBreaker = CircuitBreaker.of("cacheBreaker", config);
}
public String get(String key) {
// 使用熔断器包装查询逻辑
try {
return CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
String value = jedis.get(key);
if (value == null) {
// 查询数据库
value = queryFromDB(key);
if (value != null) {
jedis.setex(key, 3600, value);
}
}
return value;
}).get();
} catch (Exception e) {
// 熔断后返回降级数据
return getFallbackValue(key);
}
}
private String getFallbackValue(String key) {
// 返回降级数据或默认值
return "default_value";
}
private String queryFromDB(String key) {
return "data:" + key;
}
}
方案四:缓存预热
在系统启动或低峰期,提前加载热点数据到缓存。
// 缓存预热实现
public class CacheWarmUp {
private Jedis jedis;
// 应用启动时执行预热
@PostConstruct
public void warmUp() {
// 1. 加载热点数据
List<String> hotKeys = loadHotKeys();
// 2. 批量写入缓存(使用 Pipeline 提高效率)
Pipeline pipeline = jedis.pipelined();
for (String key : hotKeys) {
String value = queryFromDB(key);
if (value != null) {
// 随机过期时间
int expire = 3600 + new Random().nextInt(600);
pipeline.setex(key, expire, value);
}
}
pipeline.sync();
System.out.println("Cache warm-up completed: " + hotKeys.size() + " keys");
}
// 定时预热(更新热点数据)
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点
public void scheduledWarmUp() {
warmUp();
}
private List<String> loadHotKeys() {
// 从配置或日志中加载热点 key
// 例如:访问频率前 1000 的 key
return Arrays.asList("hot1", "hot2", "hot3");
}
private String queryFromDB(String key) {
return "data:" + key;
}
}
3.3 综合解决方案
// 完整的防雪崩方案
public class AntiAvalancheCache {
private Jedis jedis;
private Cache<String, String> localCache;
private CircuitBreaker circuitBreaker;
public AntiAvalancheCache() {
// 本地缓存
localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(300, TimeUnit.SECONDS)
.build();
// 熔断器
circuitBreaker = CircuitBreaker.of("cacheBreaker",
CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(100)
.build()
);
}
public String get(String key) {
// 1. 本地缓存
String value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 熔断保护下的查询
try {
value = CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
// Redis 查询
String v = jedis.get(key);
if (v == null) {
// 数据库查询(带互斥锁)
v = queryFromDBWithLock(key);
}
return v;
}).get();
// 3. 回填本地缓存
if (value != null) {
localCache.put(key, value);
}
return value;
} catch (Exception e) {
// 4. 降级处理
return getFallback(key);
}
}
private String queryFromDBWithLock(String key) {
String lockKey = "lock:" + key;
try {
// 尝试获取锁
if (tryLock(lockKey, 10)) {
// 双重检查
String value = jedis.get(key);
if (value != null) {
return value;
}
// 查询数据库
value = queryFromDB(key);
// 写入缓存(随机过期时间)
if (value != null) {
int expire = 3600 + new Random().nextInt(600);
jedis.setex(key, expire, value);
}
return value;
} else {
// 获取锁失败,等待重试
Thread.sleep(50);
String value = jedis.get(key);
return value != null ? value : queryFromDBWithLock(key);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
unlock(lockKey);
}
}
private String getFallback(String key) {
// 降级策略:返回默认值或走备份数据源
return "default_value";
}
private boolean tryLock(String key, int expireSeconds) {
return "OK".equals(jedis.set(key, "1", "NX", "EX", expireSeconds));
}
private void unlock(String key) {
jedis.del(key);
}
private String queryFromDB(String key) {
return "data:" + key;
}
}
四、缓存击穿
4.1 问题介绍
缓存击穿是指某个热点 key 过期或被删除,此时大量并发请求同时访问该 key,都发现缓存不存在,同时去查询数据库,导致数据库压力骤增。
与雪崩的区别:
- 雪崩:大量 key 同时失效
- 击穿:单个热点 key 失效
场景举例:
- 热门商品信息缓存过期
- 新闻头条缓存过期
- 限时秒杀活动数据
4.2 解决方案
方案一:互斥锁
使用分布式锁,只让一个请求查询数据库,其他请求等待。
// 互斥锁解决方案
public class MutexLockCache {
private Jedis jedis;
public String get(String key) {
// 1. 查询缓存
String value = jedis.get(key);
if (value != null) {
return value;
}
// 2. 尝试获取分布式锁
String lockKey = "lock:" + key;
try {
if (tryLock(lockKey, 10)) {
// 3. 获取锁成功,查询数据库
// 双重检查,防止其他线程已经更新缓存
value = jedis.get(key);
if (value != null) {
return value;
}
// 查询数据库
value = queryFromDB(key);
// 写入缓存
if (value != null) {
jedis.setex(key, 3600, value);
}
return value;
} else {
// 4. 获取锁失败,等待后重试
return waitAndRetry(key);
}
} finally {
unlock(lockKey);
}
}
private String waitAndRetry(String key) {
// 等待 50ms 后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return get(key);
}
private boolean tryLock(String key, int expireSeconds) {
return "OK".equals(jedis.set(key, "1", "NX", "EX", expireSeconds));
}
private void unlock(String key) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
jedis.eval(script, 1, key, "1");
}
private String queryFromDB(String key) {
return "data:" + key;
}
}
方案二:热点数据永不过期
对于确定的热点数据,可以设置为永不过期,通过后台任务定期更新。
// 热点数据永不过期方案
public class HotDataCache {
private Jedis jedis;
// 物理过期时间(实际存储)
private static final int PHYSICAL_EXPIRE = 86400; // 24小时
// 逻辑过期时间(数据中的字段)
private static final int LOGICAL_EXPIRE = 3600; // 1小时
// 存储数据(包含逻辑过期时间)
public void set(String key, String value) {
CacheData data = new CacheData(value, System.currentTimeMillis() + LOGICAL_EXPIRE * 1000);
jedis.setex(key, PHYSICAL_EXPIRE, JSON.toJSONString(data));
}
// 获取数据
public String get(String key) {
String json = jedis.get(key);
if (json == null) {
// 真正过期,查询数据库
String value = queryFromDB(key);
if (value != null) {
set(key, value);
}
return value;
}
CacheData data = JSON.parseObject(json, CacheData.class);
// 检查逻辑过期
if (data.expireTime < System.currentTimeMillis()) {
// 逻辑过期,异步更新
asyncRefresh(key);
}
return data.value;
}
// 异步刷新缓存
private void asyncRefresh(String key) {
CompletableFuture.runAsync(() -> {
String value = queryFromDB(key);
if (value != null) {
set(key, value);
}
});
}
static class CacheData {
String value;
long expireTime;
CacheData(String value, long expireTime) {
this.value = value;
this.expireTime = expireTime;
}
}
private String queryFromDB(String key) {
return "data:" + key;
}
}
方案三:提前续期
在缓存即将过期前,提前续期,保证热点数据不会过期。
// 提前续期方案
public class RenewCache {
private Jedis jedis;
// 过期时间
private static final int EXPIRE_TIME = 3600; // 1小时
// 续期阈值(剩余时间小于此值则续期)
private static final int RENEW_THRESHOLD = 600; // 10分钟
public String get(String key) {
String value = jedis.get(key);
if (value != null) {
// 检查剩余时间
long ttl = jedis.ttl(key);
if (ttl > 0 && ttl < RENEW_THRESHOLD) {
// 异步续期
asyncRenew(key, value);
}
return value;
}
// 查询数据库
value = queryFromDB(key);
if (value != null) {
jedis.setex(key, EXPIRE_TIME, value);
}
return value;
}
private void asyncRenew(String key, String value) {
CompletableFuture.runAsync(() -> {
// 重新设置过期时间
jedis.setex(key, EXPIRE_TIME, value);
});
}
private String queryFromDB(String key) {
return "data:" + key;
}
}
五、综合缓存架构
5.1 完整缓存方案
// 完整的缓存架构实现
public class CompleteCacheSystem {
// 一级缓存:本地缓存
private Cache<String, CacheData> localCache;
// 二级缓存:Redis
private Jedis jedis;
// 布隆过滤器
private BloomFilter<String> bloomFilter;
// 熔断器
private CircuitBreaker circuitBreaker;
public CompleteCacheSystem() {
// 初始化本地缓存
localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(300, TimeUnit.SECONDS)
.build();
// 初始化布隆过滤器
bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, 0.01
);
loadBloomFilter();
// 初始化熔断器
circuitBreaker = CircuitBreaker.of("cacheBreaker",
CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(100)
.build()
);
}
public String get(String key) {
// 1. 参数校验
if (!isValidKey(key)) {
return null;
}
// 2. 布隆过滤器检查(防穿透)
if (!bloomFilter.mightContain(key)) {
return null;
}
// 3. 本地缓存查询
CacheData localData = localCache.getIfPresent(key);
if (localData != null) {
// 检查逻辑过期
if (localData.expireTime > System.currentTimeMillis()) {
return localData.value;
}
// 逻辑过期,异步更新
asyncRefresh(key);
return localData.value;
}
// 4. Redis 查询(带熔断保护)
try {
String value = CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
return getFromRedis(key);
}).get();
// 5. 回填本地缓存
if (value != null) {
localCache.put(key, new CacheData(value,
System.currentTimeMillis() + 300 * 1000));
}
return value;
} catch (Exception e) {
// 6. 降级处理
return getFallback(key);
}
}
private String getFromRedis(String key) {
String value = jedis.get(key);
if (value != null) {
// 检查是否需要续期(防击穿)
long ttl = jedis.ttl(key);
if (ttl > 0 && ttl < 600) {
asyncRenew(key, value);
}
return value;
}
// 使用互斥锁查询数据库(防击穿)
return queryFromDBWithLock(key);
}
private String queryFromDBWithLock(String key) {
String lockKey = "lock:" + key;
try {
if (tryLock(lockKey, 10)) {
// 双重检查
String value = jedis.get(key);
if (value != null) {
return value;
}
// 查询数据库
value = queryFromDB(key);
// 写入缓存(随机过期时间,防雪崩)
if (value != null) {
int expire = 3600 + new Random().nextInt(600);
jedis.setex(key, expire, value);
// 更新布隆过滤器
bloomFilter.put(key);
} else {
// 缓存空值(防穿透)
jedis.setex(key, 60, "NULL");
}
return value;
} else {
// 等待重试
Thread.sleep(50);
return jedis.get(key);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
unlock(lockKey);
}
}
private void asyncRefresh(String key) {
CompletableFuture.runAsync(() -> {
String value = queryFromDB(key);
if (value != null) {
int expire = 3600 + new Random().nextInt(600);
jedis.setex(key, expire, value);
localCache.put(key, new CacheData(value,
System.currentTimeMillis() + 300 * 1000));
}
});
}
private void asyncRenew(String key, String value) {
CompletableFuture.runAsync(() -> {
jedis.setex(key, 3600, value);
});
}
private void loadBloomFilter() {
// 从数据库加载所有 key 到布隆过滤器
List<String> keys = loadAllKeys();
for (String key : keys) {
bloomFilter.put(key);
}
}
private String getFallback(String key) {
// 返回降级数据
return "default_value";
}
private boolean isValidKey(String key) {
return key != null && key.matches("^\\w+:\\d+$");
}
private boolean tryLock(String key, int expireSeconds) {
return "OK".equals(jedis.set(key, "1", "NX", "EX", expireSeconds));
}
private void unlock(String key) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
jedis.eval(script, 1, key, "1");
}
private List<String> loadAllKeys() {
return new ArrayList<>();
}
private String queryFromDB(String key) {
return "data:" + key;
}
static class CacheData {
String value;
long expireTime;
CacheData(String value, long expireTime) {
this.value = value;
this.expireTime = expireTime;
}
}
}
六、总结与最佳实践
6.1 问题对比总结
| 问题 | 原因 | 危害 | 解决方案 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 数据库压力增大 | 布隆过滤器、缓存空值、参数校验 |
| 缓存雪崩 | 大量缓存同时失效 | 数据库压力骤增 | 随机过期时间、多级缓存、熔断降级 |
| 缓存击穿 | 热点 key 过期 | 数据库压力大 | 互斥锁、永不过期、提前续期 |
6.2 最佳实践
-
缓存穿透防护
- 使用布隆过滤器预过滤
- 缓存空值(短过期时间)
- 严格的参数校验
-
缓存雪崩防护
- 过期时间加随机值
- 使用多级缓存
- 实施熔断降级
- 做好缓存预热
-
缓存击穿防护
- 使用分布式互斥锁
- 热点数据永不过期
- 提前续期机制
-
监控告警
- 监控缓存命中率
- 监控数据库查询量
- 设置合理的告警阈值
-
容灾设计
- Redis 高可用部署
- 数据库限流
- 服务降级预案
七、思考与练习
思考题
-
基础题:缓存穿透、缓存雪崩、缓存击穿三者有什么区别?请分别描述它们的典型场景。
-
进阶题:布隆过滤器为什么可能产生误判?在实际项目中如何降低误判率?误判会带来什么影响?
-
实战题:设计一个电商商品详情页的缓存方案,需要同时防护穿透、雪崩、击穿三种问题,请画出架构图并说明关键设计点。
编程练习
练习:实现一个带自动预热和智能续期的热点数据缓存管理器,要求:
- 支持设置热点数据标识
- 热点数据即将过期前自动异步续期
- 非热点数据过期后正常重新加载
- 提供统计数据接口(命中率、续期次数等)
提示:使用 Redis 的 TTL 命令获取剩余时间,结合线程池实现异步续期。
章节关联
- 前置章节:《Redis集群详解》- 掌握 Redis 高可用方案
- 后续章节:《缓存一致性详解》- 深入学习缓存与数据库的一致性问题
- 扩展阅读:《大型网站技术架构》缓存章节
📝 下一章预告
下一章将深入探讨缓存一致性问题,包括缓存更新策略、强一致性方案、最终一致性保证等核心话题,解决分布式系统中最棘手的数据同步难题。
本章完