缓存预热、雪崩、穿透、击穿详解
概述
在高并发系统中,缓存是提升性能的关键技术。然而,缓存使用不当会引发各种问题,本文将详细讲解缓存预热、缓存雪崩、缓存穿透、缓存击穿四个重要概念,并提供相应的解决方案。
1. 缓存预热 (Cache Warming)
1.1 概念定义
缓存预热是指在系统启动或缓存失效后,主动将热点数据加载到缓存中,避免用户请求时出现缓存未命中的情况。
1.2 场景说明
sequenceDiagram
participant User as 用户
participant App as 应用服务
participant Cache as 缓存
participant DB as 数据库
Note over User,DB: 系统启动时缓存为空
User->>App: 请求热点数据
App->>Cache: 查询缓存
Cache-->>App: 缓存未命中
App->>DB: 查询数据库
DB-->>App: 返回数据
App->>Cache: 写入缓存
App-->>User: 返回数据
Note over User,DB: 大量用户同时请求
User->>App: 大量并发请求
App->>DB: 大量数据库查询
Note over DB: 数据库压力过大
1.3 问题分析
graph TD
A[系统启动] --> B[缓存为空]
B --> C[用户请求]
C --> D[缓存未命中]
D --> E[数据库查询]
E --> F{并发量大?}
F -->|是| G[数据库压力过大]
F -->|否| H[正常响应]
G --> I[响应延迟]
G --> J[可能宕机]
style G fill:#ffcdd2
style I fill:#ffcdd2
style J fill:#ffcdd2
1.4 解决方案
1.4.1 定时预热策略
flowchart TD
START([系统启动]) --> SCHEDULE[启动定时任务]
SCHEDULE --> IDENTIFY[识别热点数据]
IDENTIFY --> BATCH[批量加载数据]
BATCH --> CACHE_LOAD[写入缓存]
CACHE_LOAD --> MONITOR[监控缓存状态]
MONITOR --> READY[系统就绪]
READY --> CHECK{定期检查}
CHECK -->|缓存过期| REFRESH[刷新缓存]
CHECK -->|缓存正常| READY
REFRESH --> CACHE_LOAD
style START fill:#e8f5e8
style READY fill:#e1f5fe
style REFRESH fill:#fff3e0
1.4.2 预热实现代码
@Component
public class CacheWarmupService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductService productService;
@Autowired
private UserService userService;
/**
* 系统启动时执行缓存预热
*/
@PostConstruct
public void warmupCache() {
log.info("开始执行缓存预热...");
// 预热热门商品
warmupHotProducts();
// 预热用户基础信息
warmupUserProfiles();
// 预热配置信息
warmupSystemConfig();
log.info("缓存预热完成");
}
/**
* 预热热门商品
*/
private void warmupHotProducts() {
try {
// 获取热门商品列表
List<Product> hotProducts = productService.getHotProducts(1000);
// 批量写入缓存
Map<String, Object> cacheData = new HashMap<>();
for (Product product : hotProducts) {
String key = "product:" + product.getId();
cacheData.put(key, product);
}
// 使用管道批量操作
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
cacheData.forEach((key, value) -> {
connection.setEx(key.getBytes(), 3600,
JSON.toJSONString(value).getBytes());
});
return null;
});
log.info("预热热门商品完成,共{}个", hotProducts.size());
} catch (Exception e) {
log.error("预热热门商品失败", e);
}
}
/**
* 预热用户基础信息
*/
private void warmupUserProfiles() {
try {
// 获取活跃用户列表
List<Long> activeUserIds = userService.getActiveUserIds(5000);
// 分批处理,避免内存溢出
int batchSize = 100;
for (int i = 0; i < activeUserIds.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, activeUserIds.size());
List<Long> batch = activeUserIds.subList(i, endIndex);
// 批量查询用户信息
List<UserProfile> profiles = userService.getUserProfiles(batch);
// 写入缓存
for (UserProfile profile : profiles) {
String key = "user:profile:" + profile.getUserId();
redisTemplate.opsForValue().set(key, profile,
Duration.ofMinutes(30));
}
}
log.info("预热用户基础信息完成,共{}个", activeUserIds.size());
} catch (Exception e) {
log.error("预热用户基础信息失败", e);
}
}
/**
* 预热系统配置
*/
private void warmupSystemConfig() {
try {
// 预热系统配置
Map<String, Object> configs = configService.getAllConfigs();
configs.forEach((key, value) -> {
redisTemplate.opsForValue().set("config:" + key, value,
Duration.ofHours(24));
});
log.info("预热系统配置完成,共{}项", configs.size());
} catch (Exception e) {
log.error("预热系统配置失败", e);
}
}
/**
* 定时刷新缓存
*/
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void refreshCache() {
// 检查缓存命中率
double hitRate = getCacheHitRate();
if (hitRate < 0.8) { // 命中率低于80%时重新预热
log.warn("缓存命中率过低: {}%,开始重新预热", hitRate * 100);
warmupCache();
}
}
private double getCacheHitRate() {
// 实现缓存命中率计算逻辑
return 0.85; // 示例值
}
}
1.4.3 智能预热策略
graph LR
subgraph "数据分析"
A1[访问日志分析]
A2[用户行为分析]
A3[业务规则分析]
end
subgraph "热点识别"
B1[访问频率排序]
B2[时间段分析]
B3[地域分析]
end
subgraph "预热策略"
C1[全量预热]
C2[增量预热]
C3[按需预热]
end
subgraph "执行方式"
D1[启动时预热]
D2[定时预热]
D3[触发式预热]
end
A1 --> B1
A2 --> B2
A3 --> B3
B1 --> C1
B2 --> C2
B3 --> C3
C1 --> D1
C2 --> D2
C3 --> D3
style A1 fill:#e1f5fe
style B1 fill:#e8f5e8
style C1 fill:#fff3e0
style D1 fill:#ffebee
2. 缓存雪崩 (Cache Avalanche)
2.1 概念定义
缓存雪崩是指大量缓存在同一时间失效,导致大量请求直接访问数据库,造成数据库压力骤增甚至宕机的现象。
2.2 场景说明
sequenceDiagram
participant Users as 大量用户
participant App as 应用服务
participant Cache as 缓存集群
participant DB as 数据库
Note over Users,DB: 正常情况下
Users->>App: 并发请求
App->>Cache: 查询缓存
Cache-->>App: 缓存命中
App-->>Users: 快速响应
Note over Users,DB: 缓存雪崩发生
Note over Cache: 大量缓存同时失效
Users->>App: 大量并发请求
App->>Cache: 查询缓存
Cache-->>App: 全部未命中
App->>DB: 大量数据库请求
Note over DB: 数据库压力激增
DB-->>App: 响应缓慢/超时
App-->>Users: 响应延迟/失败
2.3 问题分析
graph TD
A[缓存雪崩触发] --> B{原因分析}
B -->|原因1| C[缓存同时过期]
B -->|原因2| D[缓存服务宕机]
B -->|原因3| E[网络故障]
C --> F[大量请求打到数据库]
D --> F
E --> F
F --> G[数据库连接池耗尽]
F --> H[数据库响应变慢]
F --> I[数据库可能宕机]
G --> J[服务不可用]
H --> J
I --> J
J --> K[用户体验极差]
J --> L[业务损失]
style A fill:#ffcdd2
style F fill:#ffcdd2
style J fill:#ffcdd2
style K fill:#ffcdd2
style L fill:#ffcdd2
2.4 解决方案
2.4.1 过期时间随机化
flowchart TD
START[设置缓存] --> BASE_TIME[基础过期时间]
BASE_TIME --> RANDOM[添加随机时间]
RANDOM --> FINAL_TIME[最终过期时间]
BASE_TIME --> EXAMPLE1["基础时间: 3600秒"]
RANDOM --> EXAMPLE2["随机时间: 0-300秒"]
FINAL_TIME --> EXAMPLE3["最终时间: 3600-3900秒"]
EXAMPLE3 --> RESULT[避免同时过期]
style START fill:#e8f5e8
style RESULT fill:#e1f5fe
@Service
public class CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final Random RANDOM = new Random();
/**
* 设置缓存,避免雪崩
*/
public void setCache(String key, Object value, int baseExpireSeconds) {
// 基础过期时间 + 随机时间(0-300秒)
int randomExpire = baseExpireSeconds + RANDOM.nextInt(300);
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(randomExpire));
}
/**
* 批量设置缓存,每个key的过期时间都不同
*/
public void setBatchCache(Map<String, Object> dataMap, int baseExpireSeconds) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
dataMap.forEach((key, value) -> {
int randomExpire = baseExpireSeconds + RANDOM.nextInt(300);
connection.setEx(key.getBytes(), randomExpire,
JSON.toJSONString(value).getBytes());
});
return null;
});
}
}
2.4.2 多级缓存架构
graph TB
subgraph "客户端"
A[用户请求]
end
subgraph "应用层"
B[本地缓存 L1]
C[应用服务]
end
subgraph "缓存层"
D[Redis主缓存 L2]
E[Redis备用缓存 L3]
end
subgraph "数据层"
F[数据库]
end
A --> B
B -->|未命中| C
C --> D
D -->|未命中| E
E -->|未命中| F
F --> E
E --> D
D --> C
C --> B
B --> A
style B fill:#e1f5fe
style D fill:#e8f5e8
style E fill:#fff3e0
style F fill:#ffebee
@Service
public class MultiLevelCacheService {
@Autowired
private RedisTemplate<String, Object> primaryRedis;
@Autowired
private RedisTemplate<String, Object> backupRedis;
// 本地缓存
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofMinutes(5))
.build();
/**
* 多级缓存查询
*/
public Object get(String key) {
// L1: 本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// L2: 主Redis缓存
value = primaryRedis.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
return value;
}
// L3: 备用Redis缓存
value = backupRedis.opsForValue().get(key);
if (value != null) {
// 异步恢复主缓存
CompletableFuture.runAsync(() -> {
primaryRedis.opsForValue().set(key, value, Duration.ofMinutes(30));
});
localCache.put(key, value);
return value;
}
return null;
}
/**
* 多级缓存设置
*/
public void set(String key, Object value, Duration expire) {
// 同时设置多级缓存
localCache.put(key, value);
primaryRedis.opsForValue().set(key, value, expire);
backupRedis.opsForValue().set(key, value, expire.plusMinutes(10)); // 备用缓存延长10分钟
}
}
2.4.3 熔断降级机制
stateDiagram-v2
[*] --> 正常状态
正常状态 --> 半开状态 : 错误率超过阈值
半开状态 --> 正常状态 : 请求成功
半开状态 --> 熔断状态 : 请求失败
熔断状态 --> 半开状态 : 超时后尝试
正常状态 : 正常处理请求
半开状态 : 限制请求量测试
熔断状态 : 直接返回降级数据
@Component
public class CircuitBreakerCacheService {
private final CircuitBreaker circuitBreaker;
public CircuitBreakerCacheService() {
this.circuitBreaker = CircuitBreaker.ofDefaults("cacheService");
// 配置熔断器
circuitBreaker.getEventPublisher()
.onStateTransition(event ->
log.info("熔断器状态变化: {} -> {}",
event.getStateTransition().getFromState(),
event.getStateTransition().getToState()));
}
/**
* 带熔断的缓存查询
*/
public Object getWithCircuitBreaker(String key) {
Supplier<Object> cacheSupplier = CircuitBreaker
.decorateSupplier(circuitBreaker, () -> {
return redisTemplate.opsForValue().get(key);
});
try {
return cacheSupplier.get();
} catch (Exception e) {
log.warn("缓存查询失败,返回降级数据: {}", key);
return getFallbackData(key);
}
}
/**
* 获取降级数据
*/
private Object getFallbackData(String key) {
// 返回默认值或从其他数据源获取
if (key.startsWith("product:")) {
return getDefaultProduct();
} else if (key.startsWith("user:")) {
return getDefaultUser();
}
return null;
}
}
3. 缓存穿透 (Cache Penetration)
3.1 概念定义
缓存穿透是指查询一个不存在的数据,由于缓存和数据库都没有该数据,每次请求都会穿透缓存直接查询数据库。
3.2 场景说明
sequenceDiagram
participant Attacker as 恶意用户
participant App as 应用服务
participant Cache as 缓存
participant DB as 数据库
Note over Attacker,DB: 正常查询存在的数据
Attacker->>App: 查询ID=1的用户
App->>Cache: 查询缓存
Cache-->>App: 缓存命中
App-->>Attacker: 返回用户信息
Note over Attacker,DB: 恶意查询不存在的数据
Attacker->>App: 查询ID=-1的用户
App->>Cache: 查询缓存
Cache-->>App: 缓存未命中
App->>DB: 查询数据库
DB-->>App: 数据不存在
Note over App: 不缓存空结果
App-->>Attacker: 返回空
Note over Attacker,DB: 大量恶意请求
loop 大量并发请求
Attacker->>App: 查询不存在的数据
App->>Cache: 缓存未命中
App->>DB: 数据库查询
DB-->>App: 数据不存在
end
Note over DB: 数据库压力过大
3.3 问题分析
graph TD
A[恶意请求] --> B[查询不存在的数据]
B --> C[缓存未命中]
C --> D[数据库查询]
D --> E[数据不存在]
E --> F[不缓存空结果]
F --> G[下次请求重复流程]
G --> H[大量无效查询]
H --> I[数据库资源浪费]
H --> J[系统性能下降]
H --> K[可能导致宕机]
style A fill:#ffcdd2
style H fill:#ffcdd2
style I fill:#ffcdd2
style J fill:#ffcdd2
style K fill:#ffcdd2
3.4 解决方案
3.4.1 缓存空值
flowchart TD
START[接收请求] --> CACHE_CHECK[检查缓存]
CACHE_CHECK --> CACHE_HIT{缓存命中?}
CACHE_HIT -->|是| RETURN_CACHE[返回缓存数据]
CACHE_HIT -->|否| DB_QUERY[查询数据库]
DB_QUERY --> DB_RESULT{数据存在?}
DB_RESULT -->|是| CACHE_DATA[缓存数据]
DB_RESULT -->|否| CACHE_NULL[缓存空值]
CACHE_DATA --> RETURN_DATA[返回数据]
CACHE_NULL --> SET_SHORT_TTL[设置较短过期时间]
SET_SHORT_TTL --> RETURN_NULL[返回空]
RETURN_CACHE --> END[结束]
RETURN_DATA --> END
RETURN_NULL --> END
style CACHE_NULL fill:#fff3e0
style SET_SHORT_TTL fill:#fff3e0
@Service
public class CachePenetrationService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String NULL_VALUE = "NULL";
private static final int NULL_TTL = 300; // 空值缓存5分钟
/**
* 防穿透的用户查询
*/
public User getUserById(Long userId) {
String key = "user:" + userId;
// 查询缓存
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
if (NULL_VALUE.equals(cached)) {
return null; // 缓存的空值
}
return (User) cached;
}
// 查询数据库
User user = userMapper.selectById(userId);
if (user != null) {
// 缓存真实数据
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
} else {
// 缓存空值,防止穿透
redisTemplate.opsForValue().set(key, NULL_VALUE, Duration.ofSeconds(NULL_TTL));
}
return user;
}
/**
* 通用的防穿透查询方法
*/
public <T> T getWithAntiPenetration(String key, Class<T> type,
Supplier<T> dataLoader) {
// 查询缓存
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
if (NULL_VALUE.equals(cached)) {
return null;
}
return type.cast(cached);
}
// 加载数据
T data = dataLoader.get();
if (data != null) {
redisTemplate.opsForValue().set(key, data, Duration.ofMinutes(30));
} else {
redisTemplate.opsForValue().set(key, NULL_VALUE, Duration.ofSeconds(NULL_TTL));
}
return data;
}
}
3.4.2 布隆过滤器
graph LR
subgraph "请求处理流程"
A[用户请求] --> B[布隆过滤器检查]
B --> C{可能存在?}
C -->|否| D[直接返回空]
C -->|是| E[查询缓存]
E --> F{缓存命中?}
F -->|是| G[返回缓存数据]
F -->|否| H[查询数据库]
H --> I[返回结果]
end
subgraph "布隆过滤器"
J[位数组]
K[哈希函数1]
L[哈希函数2]
M[哈希函数3]
end
B --> J
B --> K
B --> L
B --> M
style D fill:#e8f5e8
style J fill:#e1f5fe
@Service
public class BloomFilterCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
// 布隆过滤器
private BloomFilter<Long> userBloomFilter;
@PostConstruct
public void initBloomFilter() {
// 创建布隆过滤器,预期100万用户,假阳性率0.01%
userBloomFilter = BloomFilter.create(
Funnels.longFunnel(),
1_000_000,
0.0001
);
// 将所有存在的用户ID添加到布隆过滤器
List<Long> allUserIds = userMapper.getAllUserIds();
allUserIds.forEach(userBloomFilter::put);
log.info("布隆过滤器初始化完成,加载{}个用户ID", allUserIds.size());
}
/**
* 使用布隆过滤器防穿透的查询
*/
public User getUserByIdWithBloomFilter(Long userId) {
// 布隆过滤器快速判断
if (!userBloomFilter.mightContain(userId)) {
log.info("布隆过滤器判断用户{}不存在,直接返回", userId);
return null; // 一定不存在
}
String key = "user:" + userId;
// 查询缓存
User cached = (User) redisTemplate.opsForValue().get(key);
if (cached != null) {
return cached;
}
// 查询数据库(可能存在)
User user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
}
return user;
}
/**
* 新增用户时更新布隆过滤器
*/
public User createUser(User user) {
User savedUser = userMapper.insert(user);
// 添加到布隆过滤器
userBloomFilter.put(savedUser.getId());
// 更新缓存
String key = "user:" + savedUser.getId();
redisTemplate.opsForValue().set(key, savedUser, Duration.ofMinutes(30));
return savedUser;
}
}
3.4.3 参数校验
flowchart TD
START[接收请求] --> VALIDATE[参数校验]
VALIDATE --> VALID{参数合法?}
VALID -->|否| REJECT[拒绝请求]
VALID -->|是| WHITELIST[白名单检查]
WHITELIST --> INLIST{在白名单?}
INLIST -->|否| RATE_LIMIT[频率限制]
INLIST -->|是| PROCESS[正常处理]
RATE_LIMIT --> EXCEED{超过限制?}
EXCEED -->|是| REJECT
EXCEED -->|否| PROCESS
REJECT --> LOG[记录日志]
PROCESS --> CACHE[缓存查询]
style VALIDATE fill:#e1f5fe
style REJECT fill:#ffcdd2
style RATE_LIMIT fill:#fff3e0
@RestController
public class UserController {
@Autowired
private BloomFilterCacheService cacheService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 带参数校验的用户查询
*/
@GetMapping("/user/{userId}")
public ResponseEntity<User> getUser(@PathVariable Long userId,
HttpServletRequest request) {
// 1. 参数校验
if (userId == null || userId <= 0) {
log.warn("非法用户ID: {}", userId);
return ResponseEntity.badRequest().build();
}
// 2. 频率限制
String clientIp = getClientIp(request);
if (!checkRateLimit(clientIp)) {
log.warn("IP {} 请求频率过高", clientIp);
return ResponseEntity.status(429).build(); // Too Many Requests
}
// 3. 查询用户
User user = cacheService.getUserByIdWithBloomFilter(userId);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}
/**
* 检查请求频率限制
*/
private boolean checkRateLimit(String clientIp) {
String key = "rate_limit:" + clientIp;
String script =
"local current = redis.call('GET', KEYS[1])\n" +
"if current == false then\n" +
" redis.call('SET', KEYS[1], 1)\n" +
" redis.call('EXPIRE', KEYS[1], ARGV[1])\n" +
" return 1\n" +
"else\n" +
" local count = redis.call('INCR', KEYS[1])\n" +
" if count > tonumber(ARGV[2]) then\n" +
" return 0\n" +
" else\n" +
" return 1\n" +
" end\n" +
"end";
Long result = redisTemplate.execute((RedisCallback<Long>) connection -> {
return (Long) connection.eval(
script.getBytes(),
ReturnType.INTEGER,
1,
key.getBytes(),
"60".getBytes(), // 60秒窗口
"100".getBytes() // 最多100次请求
);
});
return result != null && result == 1;
}
private String getClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
4. 缓存击穿 (Cache Breakdown)
4.1 概念定义
缓存击穿是指热点数据的缓存过期时,大量并发请求同时访问该数据,导致请求直接打到数据库的现象。
4.2 场景说明
sequenceDiagram
participant Users as 大量用户
participant App as 应用服务
participant Cache as 缓存
participant DB as 数据库
Note over Users,DB: 热点数据缓存有效期内
Users->>App: 并发请求热点数据
App->>Cache: 查询缓存
Cache-->>App: 缓存命中
App-->>Users: 快速响应
Note over Cache: 热点数据缓存过期
Note over Users,DB: 缓存过期瞬间大量请求
par 并发请求1
Users->>App: 请求热点数据
App->>Cache: 查询缓存
Cache-->>App: 缓存未命中
App->>DB: 查询数据库
and 并发请求2
Users->>App: 请求热点数据
App->>Cache: 查询缓存
Cache-->>App: 缓存未命中
App->>DB: 查询数据库
and 并发请求N
Users->>App: 请求热点数据
App->>Cache: 查询缓存
Cache-->>App: 缓存未命中
App->>DB: 查询数据库
end
Note over DB: 数据库瞬间压力激增
DB-->>App: 响应缓慢
App-->>Users: 响应延迟
4.3 问题分析
graph TD
A[热点数据] --> B[缓存过期]
B --> C[大量并发请求]
C --> D[缓存未命中]
D --> E[同时查询数据库]
E --> F[数据库连接数激增]
E --> G[数据库CPU飙升]
E --> H[查询响应变慢]
F --> I[连接池耗尽]
G --> J[其他查询受影响]
H --> K[用户体验下降]
I --> L[服务不可用]
J --> L
K --> L
style A fill:#fff3e0
style C fill:#ffcdd2
style E fill:#ffcdd2
style L fill:#ffcdd2
4.4 解决方案
4.4.1 互斥锁 (Mutex Lock)
sequenceDiagram
participant T1 as 线程1
participant T2 as 线程2
participant T3 as 线程3
participant Lock as 分布式锁
participant Cache as 缓存
participant DB as 数据库
Note over T1,DB: 缓存过期,多线程并发请求
par 获取锁竞争
T1->>Lock: 尝试获取锁
Lock-->>T1: 获取成功
and
T2->>Lock: 尝试获取锁
Lock-->>T2: 获取失败,等待
and
T3->>Lock: 尝试获取锁
Lock-->>T3: 获取失败,等待
end
T1->>Cache: 再次检查缓存
Cache-->>T1: 仍未命中
T1->>DB: 查询数据库
DB-->>T1: 返回数据
T1->>Cache: 更新缓存
T1->>Lock: 释放锁
Lock-->>T2: 通知锁释放
T2->>Cache: 查询缓存
Cache-->>T2: 缓存命中
Lock-->>T3: 通知锁释放
T3->>Cache: 查询缓存
Cache-->>T3: 缓存命中
@Service
public class MutexLockCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
/**
* 使用互斥锁防击穿
*/
public Product getProductById(Long productId) {
String key = "product:" + productId;
String lockKey = "lock:product:" + productId;
// 查询缓存
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 尝试获取分布式锁
String lockValue = UUID.randomUUID().toString();
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
if (lockAcquired != null && lockAcquired) {
try {
// 获取锁成功,再次检查缓存
product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 查询数据库
product = productMapper.selectById(productId);
if (product != null) {
// 更新缓存
redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
}
return product;
} finally {
// 释放锁(使用Lua脚本保证原子性)
releaseLock(lockKey, lockValue);
}
} else {
// 获取锁失败,等待一段时间后重试
try {
Thread.sleep(50); // 等待50ms
return getProductById(productId); // 递归重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
}
/**
* 释放分布式锁
*/
private void releaseLock(String lockKey, String lockValue) {
String script =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute((RedisCallback<Long>) connection -> {
return (Long) connection.eval(
script.getBytes(),
ReturnType.INTEGER,
1,
lockKey.getBytes(),
lockValue.getBytes()
);
});
}
}
4.4.2 逻辑过期
flowchart TD
START[请求到达] --> CHECK_CACHE[检查缓存]
CHECK_CACHE --> CACHE_EXISTS{缓存存在?}
CACHE_EXISTS -->|否| LOAD_DATA[加载数据]
CACHE_EXISTS -->|是| CHECK_EXPIRE[检查逻辑过期时间]
CHECK_EXPIRE --> EXPIRED{已过期?}
EXPIRED -->|否| RETURN_CACHE[返回缓存数据]
EXPIRED -->|是| TRY_LOCK[尝试获取锁]
TRY_LOCK --> LOCK_SUCCESS{获取锁成功?}
LOCK_SUCCESS -->|是| ASYNC_REFRESH[异步刷新缓存]
LOCK_SUCCESS -->|否| RETURN_OLD[返回旧数据]
ASYNC_REFRESH --> RETURN_OLD
LOAD_DATA --> SET_CACHE[设置缓存]
SET_CACHE --> RETURN_DATA[返回数据]
style RETURN_OLD fill:#fff3e0
style ASYNC_REFRESH fill:#e8f5e8
style RETURN_CACHE fill:#e1f5fe
@Service
public class LogicalExpireCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
@Autowired
private ThreadPoolExecutor refreshExecutor;
/**
* 带逻辑过期时间的缓存数据
*/
@Data
public static class CacheData {
private Object data;
private LocalDateTime expireTime;
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime);
}
}
/**
* 使用逻辑过期防击穿
*/
public Product getProductByIdWithLogicalExpire(Long productId) {
String key = "product:logical:" + productId;
String lockKey = "lock:refresh:" + productId;
// 查询缓存
CacheData cacheData = (CacheData) redisTemplate.opsForValue().get(key);
if (cacheData == null) {
// 缓存不存在,直接查询数据库
return loadAndCacheProduct(productId);
}
if (!cacheData.isExpired()) {
// 未过期,直接返回
return (Product) cacheData.getData();
}
// 逻辑过期,尝试获取锁进行异步刷新
String lockValue = UUID.randomUUID().toString();
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
if (lockAcquired != null && lockAcquired) {
// 获取锁成功,异步刷新缓存
refreshExecutor.submit(() -> {
try {
Product product = productMapper.selectById(productId);
if (product != null) {
CacheData newCacheData = new CacheData();
newCacheData.setData(product);
newCacheData.setExpireTime(LocalDateTime.now().plusMinutes(30));
redisTemplate.opsForValue().set(key, newCacheData);
}
} finally {
releaseLock(lockKey, lockValue);
}
});
}
// 返回旧数据(保证可用性)
return (Product) cacheData.getData();
}
/**
* 加载数据并缓存
*/
private Product loadAndCacheProduct(Long productId) {
Product product = productMapper.selectById(productId);
if (product != null) {
CacheData cacheData = new CacheData();
cacheData.setData(product);
cacheData.setExpireTime(LocalDateTime.now().plusMinutes(30));
String key = "product:logical:" + productId;
redisTemplate.opsForValue().set(key, cacheData);
}
return product;
}
private void releaseLock(String lockKey, String lockValue) {
// 同前面的释放锁实现
}
}
4.4.3 热点数据永不过期
graph LR
subgraph "热点数据管理"
A[热点识别] --> B[数据预加载]
B --> C[设置永不过期]
C --> D[后台定时刷新]
D --> E[监控数据变化]
E --> F[主动更新缓存]
F --> D
end
subgraph "请求处理"
G[用户请求] --> H[查询缓存]
H --> I[缓存命中]
I --> J[返回数据]
end
C --> H
style C fill:#e8f5e8
style I fill:#e1f5fe
style D fill:#fff3e0
@Service
public class HotDataCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
// 热点数据列表
private final Set<Long> hotProductIds = ConcurrentHashMap.newKeySet();
/**
* 标记热点数据
*/
public void markAsHotData(Long productId) {
hotProductIds.add(productId);
// 立即加载到缓存
Product product = productMapper.selectById(productId);
if (product != null) {
String key = "hot:product:" + productId;
redisTemplate.opsForValue().set(key, product); // 不设置过期时间
}
}
/**
* 查询热点数据
*/
public Product getHotProduct(Long productId) {
if (!hotProductIds.contains(productId)) {
return null; // 不是热点数据
}
String key = "hot:product:" + productId;
return (Product) redisTemplate.opsForValue().get(key);
}
/**
* 定时刷新热点数据
*/
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void refreshHotData() {
for (Long productId : hotProductIds) {
try {
Product product = productMapper.selectById(productId);
if (product != null) {
String key = "hot:product:" + productId;
redisTemplate.opsForValue().set(key, product);
} else {
// 数据已删除,从热点列表移除
hotProductIds.remove(productId);
String key = "hot:product:" + productId;
redisTemplate.delete(key);
}
} catch (Exception e) {
log.error("刷新热点数据失败: productId={}", productId, e);
}
}
log.info("热点数据刷新完成,共{}个", hotProductIds.size());
}
/**
* 监听数据变化,主动更新缓存
*/
@EventListener
public void handleProductUpdate(ProductUpdateEvent event) {
Long productId = event.getProductId();
if (hotProductIds.contains(productId)) {
// 立即更新热点数据缓存
Product product = productMapper.selectById(productId);
if (product != null) {
String key = "hot:product:" + productId;
redisTemplate.opsForValue().set(key, product);
log.info("热点数据缓存已更新: productId={}", productId);
}
}
}
}
5. 性能对比分析
5.1 各种方案性能对比
| 问题类型 | 解决方案 | 优点 | 缺点 | 适用场景 | 性能影响 |
|---|---|---|---|---|---|
| 缓存预热 | 定时预热 | 实现简单,效果明显 | 可能预热无用数据 | 系统启动,定期维护 | 启动时间增加 |
| 智能预热 | 精准预热,资源利用率高 | 实现复杂,需要数据分析 | 大型系统,个性化场景 | 分析开销 | |
| 缓存雪崩 | 随机过期 | 简单有效,成本低 | 无法完全避免 | 通用场景 | 几乎无影响 |
| 多级缓存 | 可用性高,容错性强 | 复杂度高,一致性问题 | 高可用系统 | 内存开销增加 | |
| 熔断降级 | 保护系统稳定性 | 可能影响用户体验 | 关键业务系统 | 降级时性能下降 | |
| 缓存穿透 | 缓存空值 | 实现简单,立即生效 | 占用缓存空间 | 小规模攻击 | 轻微内存开销 |
| 布隆过滤器 | 内存效率高,误判率低 | 存在误判,删除困难 | 大规模数据 | 计算开销 | |
| 参数校验 | 从源头防护 | 需要业务配合 | 所有场景 | 验证开销 | |
| 缓存击穿 | 互斥锁 | 严格控制并发 | 可能造成等待 | 一般并发场景 | 锁竞争开销 |
| 逻辑过期 | 无锁等待,可用性高 | 可能返回过期数据 | 高并发场景 | 异步刷新开销 | |
| 永不过期 | 性能最优 | 数据一致性问题 | 热点数据 | 定时刷新开销 |
5.2 方案选择决策树
flowchart TD
START[缓存问题] --> TYPE{问题类型}
TYPE -->|预热| WARM_START{系统规模}
WARM_START -->|小型| WARM_SIMPLE[定时预热]
WARM_START -->|大型| WARM_SMART[智能预热]
TYPE -->|雪崩| AVALANCHE_START{可用性要求}
AVALANCHE_START -->|一般| AVALANCHE_SIMPLE[随机过期]
AVALANCHE_START -->|高| AVALANCHE_COMPLEX[多级缓存+熔断]
TYPE -->|穿透| PENETRATION_START{数据规模}
PENETRATION_START -->|小| PENETRATION_SIMPLE[缓存空值]
PENETRATION_START -->|大| PENETRATION_BLOOM[布隆过滤器]
TYPE -->|击穿| BREAKDOWN_START{并发量}
BREAKDOWN_START -->|中等| BREAKDOWN_LOCK[互斥锁]
BREAKDOWN_START -->|高| BREAKDOWN_LOGICAL[逻辑过期]
BREAKDOWN_START -->|极高| BREAKDOWN_NEVER[永不过期]
style WARM_SIMPLE fill:#e1f5fe
style AVALANCHE_SIMPLE fill:#e8f5e8
style PENETRATION_SIMPLE fill:#fff3e0
style BREAKDOWN_LOCK fill:#ffebee
6. 总结
缓存是提升系统性能的重要手段,但使用不当会引发各种问题。通过本文的详细分析,我们了解了缓存预热、雪崩、穿透、击穿四大问题的原理、场景和解决方案:
- 缓存预热: 通过定时预热和智能预热,避免冷启动问题
- 缓存雪崩: 通过随机过期、多级缓存、熔断降级,提高系统稳定性
- 缓存穿透: 通过缓存空值、布隆过滤器、参数校验,防止恶意攻击
- 缓存击穿: 通过互斥锁、逻辑过期、永不过期,处理热点数据问题
在实际应用中,需要根据具体的业务场景、技术栈和团队能力,选择合适的解决方案。同时,建立完善的监控告警体系,持续优化缓存策略,才能真正发挥缓存的价值。
记住:没有银弹,只有最适合的方案。在追求高性能的同时,也要考虑系统的可维护性、可扩展性和团队的技术水平。