文章 569
评论 5
浏览 217477
热点 Key 缓存击穿防护:过期瞬间 DB 被打垮?逻辑过期+互斥锁重建+后台预热

热点 Key 缓存击穿防护:过期瞬间 DB 被打垮?逻辑过期+互斥锁重建+后台预热

问题背景

在高并发系统中,缓存是提升性能的关键手段。但你是否遇到过这样的场景:

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并发请求200s200s(击穿)0.2s
平均响应时间200ms200ms(击穿)5ms
DB 查询次数100010001

最佳实践

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();
    }
}

总结

通过逻辑过期、互斥锁重建和后台预热的组合方案,我们可以有效防止缓存击穿:

  1. 逻辑过期:缓存过期后不立即删除,返回旧数据的同时异步刷新
  2. 互斥锁重建:只有一个请求能重建缓存,其他请求等待或返回旧数据
  3. 后台预热:在缓存过期前主动刷新,避免过期瞬间的流量冲击
  4. 多级缓存:结合本地缓存和 Redis 缓存,进一步提升性能
  5. 监控告警:实时监控缓存命中率和刷新次数,及时发现问题

互动话题:你在项目中遇到过哪些缓存相关的难题?欢迎留言讨论!


标题:热点 Key 缓存击穿防护:过期瞬间 DB 被打垮?逻辑过期+互斥锁重建+后台预热
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/09/1783155116385.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消