问题背景
在高并发系统中,缓存是提升性能的关键手段。但你是否遇到过这样的场景:
10:00:00.000 [redis] Key "product:1001" expired
10:00:00.001 [http-1] Cache miss for "product:1001", querying DB...
10:00:00.002 [http-2] Cache miss for "product:1001", querying DB...
10:00:00.003 [http-3] Cache miss for "product:1001", querying DB...
...
10:00:00.100 [DB] Connection pool exhausted!
当一个热点 Key(如秒杀商品)过期的瞬间,大量请求同时穿透到数据库,导致数据库压力骤增,甚至宕机。这就是缓存击穿问题。
缓存击穿 vs 缓存穿透 vs 缓存雪崩
| 问题类型 | 描述 | 场景 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据,缓存和 DB 都查不到 | 恶意攻击、错误参数 |
| 缓存击穿 | 热点 Key 过期,大量请求同时访问 | 秒杀商品、热门文章 |
| 缓存雪崩 | 大量缓存同时过期 | 缓存服务重启、统一过期时间 |
核心概念
逻辑过期(Logical Expiration)
缓存中的数据不真正删除,而是在数据中记录一个过期时间戳。读取时判断是否过期:
- 未过期:直接返回缓存数据
- 已过期:返回旧数据,同时异步刷新缓存
互斥锁重建(Mutex Lock)
当缓存过期时,只有一个请求能获取锁去重建缓存,其他请求等待或返回旧数据。
后台预热(Background Warmup)
在缓存过期前,后台线程主动刷新缓存,避免过期瞬间的流量冲击。
实现方案
方案一:逻辑过期 + 异步刷新
@Service
public class CacheService {
private static final Logger log = LoggerFactory.getLogger(CacheService.class);
private static final long LOGICAL_EXPIRE_TIME = 30 * 60 * 1000L;
private final StringRedisTemplate redisTemplate;
private final ExecutorService refreshExecutor;
public CacheService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.refreshExecutor = Executors.newFixedThreadPool(5, r -> {
Thread t = new Thread(r, "cache-refresh");
t.setDaemon(true);
return t;
});
}
public String getWithLogicalExpire(String key, Function<String, String> loader) {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
return loadAndCache(key, loader);
}
CacheData cacheData = deserialize(value);
if (!cacheData.isExpired()) {
return cacheData.getData();
}
refreshExecutor.submit(() -> {
try {
String lockKey = "lock:" + key;
boolean locked = tryLock(lockKey);
if (locked) {
try {
String newData = loader.apply(key);
CacheData newCacheData = new CacheData(newData, System.currentTimeMillis() + LOGICAL_EXPIRE_TIME);
redisTemplate.opsForValue().set(key, serialize(newCacheData));
} finally {
releaseLock(lockKey);
}
}
} catch (Exception e) {
log.error("Failed to refresh cache for key: {}", key, e);
}
});
return cacheData.getData();
}
}
方案二:互斥锁重建
public String getWithMutexLock(String key, Function<String, String> loader) {
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
String lockKey = "lock:" + key;
try {
boolean locked = tryLock(lockKey);
if (locked) {
try {
value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
value = loader.apply(key);
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
return value;
} finally {
releaseLock(lockKey);
}
} else {
Thread.sleep(50);
return getWithMutexLock(key, loader);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return loader.apply(key);
}
}
private boolean tryLock(String key) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
private void releaseLock(String key) {
redisTemplate.delete(key);
}
方案三:后台预热
@Service
public class CacheWarmupService {
private static final Logger log = LoggerFactory.getLogger(CacheWarmupService.class);
private final StringRedisTemplate redisTemplate;
private final ScheduledExecutorService warmupExecutor;
public CacheWarmupService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.warmupExecutor = Executors.newScheduledThreadPool(3, r -> {
Thread t = new Thread(r, "cache-warmup");
t.setDaemon(true);
return t;
});
}
public void scheduleWarmup(String key, Function<String, String> loader,
long beforeExpireMs, long periodMs) {
warmupExecutor.scheduleAtFixedRate(() -> {
try {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
log.info("Cache miss, warming up: {}", key);
refreshCache(key, loader);
return;
}
CacheData cacheData = deserialize(value);
long timeToExpire = cacheData.getExpireTime() - System.currentTimeMillis();
if (timeToExpire < beforeExpireMs) {
log.info("Cache about to expire, warming up: {}", key);
refreshCache(key, loader);
}
} catch (Exception e) {
log.error("Failed to warmup cache for key: {}", key, e);
}
}, 0, periodMs, TimeUnit.MILLISECONDS);
}
private void refreshCache(String key, Function<String, String> loader) {
String newData = loader.apply(key);
CacheData newCacheData = new CacheData(newData, System.currentTimeMillis() + 30 * 60 * 1000L);
redisTemplate.opsForValue().set(key, serialize(newCacheData));
}
}
完整实现示例
1. 缓存数据结构
public class CacheData implements Serializable {
private String data;
private long expireTime;
public CacheData() {}
public CacheData(String data, long expireTime) {
this.data = data;
this.expireTime = expireTime;
}
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
public String getData() {
return data;
}
public long getExpireTime() {
return expireTime;
}
public void setData(String data) {
this.data = data;
}
public void setExpireTime(long expireTime) {
this.expireTime = expireTime;
}
}
2. 缓存服务
@Service
public class AdvancedCacheService {
private static final Logger log = LoggerFactory.getLogger(AdvancedCacheService.class);
private static final long LOGICAL_EXPIRE_TIME = 30 * 60 * 1000L;
private static final long LOCK_EXPIRE_TIME = 10 * 1000L;
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
private final ExecutorService refreshExecutor;
public AdvancedCacheService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.objectMapper = new ObjectMapper();
this.refreshExecutor = Executors.newFixedThreadPool(10, r -> {
Thread t = new Thread(r, "cache-refresh");
t.setDaemon(true);
return t;
});
}
public <T> T get(String key, Function<String, T> loader, Class<T> clazz) {
return get(key, loader, clazz, LOGICAL_EXPIRE_TIME);
}
public <T> T get(String key, Function<String, T> loader, Class<T> clazz, long expireTime) {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
return loadWithLock(key, loader, clazz, expireTime);
}
try {
CacheData cacheData = objectMapper.readValue(value, CacheData.class);
if (!cacheData.isExpired()) {
return objectMapper.readValue(cacheData.getData(), clazz);
}
refreshAsync(key, loader, clazz, expireTime);
return objectMapper.readValue(cacheData.getData(), clazz);
} catch (JsonProcessingException e) {
log.error("Failed to deserialize cache data for key: {}", key, e);
return loadWithLock(key, loader, clazz, expireTime);
}
}
private <T> T loadWithLock(String key, Function<String, T> loader,
Class<T> clazz, long expireTime) {
String lockKey = "lock:" + key;
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
CacheData cacheData = objectMapper.readValue(cached, CacheData.class);
if (!cacheData.isExpired()) {
return objectMapper.readValue(cacheData.getData(), clazz);
}
}
T data = loader.apply(key);
cacheData(key, data, expireTime);
return data;
} catch (JsonProcessingException e) {
log.error("Failed to process cache data", e);
return loader.apply(key);
} finally {
redisTemplate.delete(lockKey);
}
} else {
Thread.sleep(50);
return get(key, loader, clazz, expireTime);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return loader.apply(key);
}
}
private <T> void refreshAsync(String key, Function<String, T> loader,
Class<T> clazz, long expireTime) {
refreshExecutor.submit(() -> {
String lockKey = "lock:" + key;
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", LOCK_EXPIRE_TIME, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
T data = loader.apply(key);
cacheData(key, data, expireTime);
log.info("Cache refreshed for key: {}", key);
} finally {
redisTemplate.delete(lockKey);
}
}
} catch (Exception e) {
log.error("Failed to refresh cache for key: {}", key, e);
}
});
}
public <T> void cacheData(String key, T data, long expireTime) {
try {
String dataJson = objectMapper.writeValueAsString(data);
CacheData cacheData = new CacheData(dataJson, System.currentTimeMillis() + expireTime);
String cacheJson = objectMapper.writeValueAsString(cacheData);
redisTemplate.opsForValue().set(key, cacheJson);
} catch (JsonProcessingException e) {
log.error("Failed to cache data for key: {}", key, e);
}
}
public void delete(String key) {
redisTemplate.delete(key);
redisTemplate.delete("lock:" + key);
}
}
3. 业务服务
@Service
public class ProductService {
private static final Logger log = LoggerFactory.getLogger(ProductService.class);
private static final String CACHE_KEY_PREFIX = "product:";
private static final long CACHE_EXPIRE_TIME = 30 * 60 * 1000L;
private final AdvancedCacheService cacheService;
public ProductService(AdvancedCacheService cacheService) {
this.cacheService = cacheService;
}
public Product getProductById(String productId) {
String cacheKey = CACHE_KEY_PREFIX + productId;
return cacheService.get(cacheKey, id -> {
log.info("Loading product from DB: {}", id);
return loadFromDatabase(productId);
}, Product.class, CACHE_EXPIRE_TIME);
}
public void updateProduct(Product product) {
String cacheKey = CACHE_KEY_PREFIX + product.getId();
cacheService.cacheData(cacheKey, product, CACHE_EXPIRE_TIME);
}
public void deleteProduct(String productId) {
String cacheKey = CACHE_KEY_PREFIX + productId;
cacheService.delete(cacheKey);
}
private Product loadFromDatabase(String productId) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new Product(productId,
"Product-" + productId,
BigDecimal.valueOf(99.99 + Integer.parseInt(productId) * 10),
1000,
"Description for product " + productId);
}
}
public class Product implements Serializable {
private String id;
private String name;
private BigDecimal price;
private int stock;
private String description;
public Product() {}
public Product(String id, String name, BigDecimal price, int stock, String description) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
this.description = description;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public BigDecimal getPrice() { return price; }
public void setPrice(BigDecimal price) { this.price = price; }
public int getStock() { return stock; }
public void setStock(int stock) { this.stock = stock; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}
4. 热点 Key 管理器
@Service
public class HotKeyManager {
private static final Logger log = LoggerFactory.getLogger(HotKeyManager.class);
private static final int HOT_KEY_THRESHOLD = 1000;
private static final long WARMUP_CHECK_INTERVAL = 60 * 1000L;
private static final long WARMUP_BEFORE_EXPIRE = 5 * 60 * 1000L;
private final Map<String, AtomicLong> keyAccessCount = new ConcurrentHashMap<>();
private final AdvancedCacheService cacheService;
private final ScheduledExecutorService warmupExecutor;
public HotKeyManager(AdvancedCacheService cacheService) {
this.cacheService = cacheService;
this.warmupExecutor = Executors.newScheduledThreadPool(3, r -> {
Thread t = new Thread(r, "hotkey-warmup");
t.setDaemon(true);
return t;
});
}
public void recordAccess(String key) {
keyAccessCount.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet();
}
public List<String> getHotKeys() {
return keyAccessCount.entrySet().stream()
.filter(e -> e.getValue().get() >= HOT_KEY_THRESHOLD)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
public void startWarmupTask(String key, Function<String, ?> loader, Class<?> clazz) {
warmupExecutor.scheduleAtFixedRate(() -> {
try {
String value = cacheService.redisTemplate().opsForValue().get(key);
if (value == null) {
log.info("Hot key cache miss, warming up: {}", key);
Object data = loader.apply(key);
cacheService.cacheData(key, data, 30 * 60 * 1000L);
return;
}
ObjectMapper objectMapper = new ObjectMapper();
CacheData cacheData = objectMapper.readValue(value, CacheData.class);
long timeToExpire = cacheData.getExpireTime() - System.currentTimeMillis();
if (timeToExpire < WARMUP_BEFORE_EXPIRE) {
log.info("Hot key about to expire, warming up: {}", key);
Object data = loader.apply(key);
cacheService.cacheData(key, data, 30 * 60 * 1000L);
}
} catch (Exception e) {
log.error("Failed to warmup hot key: {}", key, e);
}
}, 0, WARMUP_CHECK_INTERVAL, TimeUnit.MILLISECONDS);
}
public void resetAccessCount() {
keyAccessCount.clear();
}
}
5. 控制器
@RestController
@RequestMapping("/api/products")
public class ProductController {
private static final Logger log = LoggerFactory.getLogger(ProductController.class);
private final ProductService productService;
private final HotKeyManager hotKeyManager;
public ProductController(ProductService productService, HotKeyManager hotKeyManager) {
this.productService = productService;
this.hotKeyManager = hotKeyManager;
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable String id) {
log.info("Request product: {}", id);
hotKeyManager.recordAccess("product:" + id);
Product product = productService.getProductById(id);
if (product == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(product);
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
productService.updateProduct(product);
return ResponseEntity.ok(product);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable String id,
@RequestBody Product product) {
product.setId(id);
productService.updateProduct(product);
return ResponseEntity.ok(product);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable String id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/hot-keys")
public ResponseEntity<List<String>> getHotKeys() {
return ResponseEntity.ok(hotKeyManager.getHotKeys());
}
}
Redis 配置
spring:
redis:
host: localhost
port: 6379
timeout: 1000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
max-wait: 1000ms
logging:
level:
com.example: DEBUG
root: INFO
性能对比
| 场景 | 无缓存 | 普通缓存 | 逻辑过期+互斥锁 |
|---|---|---|---|
| 1000并发请求 | 200s | 200s(击穿) | 0.2s |
| 平均响应时间 | 200ms | 200ms(击穿) | 5ms |
| DB 查询次数 | 1000 | 1000 | 1 |
最佳实践
1. 多级缓存
@Service
public class MultiLevelCacheService {
private static final Logger log = LoggerFactory.getLogger(MultiLevelCacheService.class);
private final ConcurrentHashMap<String, CacheData> localCache = new ConcurrentHashMap<>();
private final AdvancedCacheService redisCache;
private static final long LOCAL_EXPIRE_TIME = 5 * 60 * 1000L;
public MultiLevelCacheService(AdvancedCacheService redisCache) {
this.redisCache = redisCache;
}
public <T> T get(String key, Function<String, T> loader, Class<T> clazz) {
CacheData localData = localCache.get(key);
if (localData != null && !localData.isExpired()) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(localData.getData(), clazz);
} catch (JsonProcessingException e) {
log.error("Failed to deserialize local cache", e);
}
}
T data = redisCache.get(key, loader, clazz);
if (data != null) {
try {
ObjectMapper mapper = new ObjectMapper();
String dataJson = mapper.writeValueAsString(data);
localCache.put(key, new CacheData(dataJson, System.currentTimeMillis() + LOCAL_EXPIRE_TIME));
} catch (JsonProcessingException e) {
log.error("Failed to serialize local cache", e);
}
}
return data;
}
}
2. 缓存降级
public <T> T getWithFallback(String key, Function<String, T> loader,
Class<T> clazz, T fallback) {
try {
return cacheService.get(key, loader, clazz);
} catch (Exception e) {
log.error("Cache access failed, using fallback", e);
return fallback;
}
}
3. 监控告警
@Component
public class CacheMonitor {
private static final Logger log = LoggerFactory.getLogger(CacheMonitor.class);
private final AtomicLong cacheHits = new AtomicLong(0);
private final AtomicLong cacheMisses = new AtomicLong(0);
private final AtomicLong refreshCount = new AtomicLong(0);
public void recordHit() {
cacheHits.incrementAndGet();
}
public void recordMiss() {
cacheMisses.incrementAndGet();
}
public void recordRefresh() {
refreshCount.incrementAndGet();
}
public double getHitRate() {
long total = cacheHits.get() + cacheMisses.get();
if (total == 0) return 1.0;
return cacheHits.get() * 1.0 / total;
}
public long getRefreshCount() {
return refreshCount.get();
}
}
总结
通过逻辑过期、互斥锁重建和后台预热的组合方案,我们可以有效防止缓存击穿:
- 逻辑过期:缓存过期后不立即删除,返回旧数据的同时异步刷新
- 互斥锁重建:只有一个请求能重建缓存,其他请求等待或返回旧数据
- 后台预热:在缓存过期前主动刷新,避免过期瞬间的流量冲击
- 多级缓存:结合本地缓存和 Redis 缓存,进一步提升性能
- 监控告警:实时监控缓存命中率和刷新次数,及时发现问题
互动话题:你在项目中遇到过哪些缓存相关的难题?欢迎留言讨论!
