SpringBoot + Redis 缓存击穿防护 + 互斥重建:热点 Key 过期时,仅一个线程回源 DB
导语
在高并发系统中,缓存是提升性能的关键手段。然而,当热点 Key 过期时,大量并发请求同时穿透缓存直接访问数据库,可能导致数据库压力骤增甚至宕机。这种现象被称为"缓存击穿"。
本文将介绍如何在 SpringBoot 应用中实现 Redis 缓存击穿防护和互斥重建机制,确保热点 Key 过期时,只有一个线程回源数据库,其他线程等待或使用旧数据,从而保护数据库免受高并发冲击。
一、缓存击穿的概念与危害
1.1 什么是缓存击穿
缓存击穿是指某个热点 Key 在高并发访问时突然过期,导致大量并发请求同时穿透缓存直接访问数据库的现象。
场景描述:
- 某个商品信息被大量用户频繁访问
- 该商品的缓存 Key 设置了过期时间
- 缓存过期瞬间,大量请求同时到达
- 所有请求都发现缓存不存在,同时访问数据库
- 数据库瞬间承受巨大压力,可能导致宕机
1.2 缓存击穿与相关概念的区别
| 概念 | 描述 | 解决方案 |
|---|---|---|
| 缓存击穿 | 热点 Key 过期,大量请求同时访问数据库 | 互斥锁、永不过期 |
| 缓存穿透 | 查询不存在的数据,请求直接访问数据库 | 布隆过滤器、缓存空值 |
| 缓存雪崩 | 大量 Key 同时过期,导致数据库压力骤增 | 过期时间随机化、预热 |
1.3 缓存击穿的危害
| 危害 | 描述 |
|---|---|
| 数据库压力 | 大量并发请求同时访问数据库 |
| 系统崩溃 | 数据库可能因过载而崩溃 |
| 响应延迟 | 请求响应时间显著增加 |
| 用户体验 | 用户等待时间过长,体验下降 |
| 成本增加 | 需要增加数据库资源应对峰值 |
二、技术方案设计
2.1 架构设计
flowchart TD
subgraph 客户端层
A[客户端请求] -->|请求| B[SpringBoot 应用]
end
subgraph 缓存层
B -->|查询| C[Redis]
C -->|缓存命中| B
C -->|缓存未命中| D[互斥锁检查]
end
subgraph 互斥层
D -->|获取锁成功| E[回源数据库]
D -->|获取锁失败| F[等待或使用旧数据]
end
subgraph 数据层
E -->|查询| G[数据库]
G -->|返回数据| E
end
subgraph 缓存重建
E -->|写入缓存| C
F -->|等待重建完成| C
end
2.2 核心组件
- 缓存服务:负责缓存的读写操作
- 互斥锁服务:负责分布式锁的获取和释放
- 缓存击穿防护服务:整合缓存和互斥锁,实现击穿防护
- 业务服务:使用缓存击穿防护的业务逻辑
2.3 技术选型
| 技术 | 版本 | 用途 |
|---|---|---|
| SpringBoot | 2.7.14 | 应用框架 |
| Spring Data Redis | 2.7.0 | Redis 客户端 |
| Redis | 7.0+ | 缓存存储 |
| Lettuce | 6.1.0 | Redis 连接池 |
| Lombok | 1.18.0 | 简化代码 |
三、核心实现
3.1 依赖配置
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson (可选,提供更强大的分布式锁功能) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.20.0</version>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3.2 缓存击穿防护服务
CacheBreakdownProtectionService.java
@Service
@Slf4j
public class CacheBreakdownProtectionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private DistributedLockService distributedLockService;
private static final String CACHE_PREFIX = "cache:";
private static final String LOCK_PREFIX = "lock:";
private static final long DEFAULT_CACHE_EXPIRE_TIME = 3600; // 默认缓存过期时间 1 小时
private static final long DEFAULT_LOCK_WAIT_TIME = 3000; // 默认获取锁等待时间 3 秒
private static final long DEFAULT_LOCK_EXPIRE_TIME = 10000; // 默认锁过期时间 10 秒
/**
* 获取缓存数据,带击穿防护
* @param key 缓存键
* @param dataLoader 数据加载器
* @param <T> 数据类型
* @return 缓存数据
*/
public <T> T getWithProtection(String key, DataLoader<T> dataLoader) {
return getWithProtection(key, dataLoader, DEFAULT_CACHE_EXPIRE_TIME);
}
/**
* 获取缓存数据,带击穿防护
* @param key 缓存键
* @param dataLoader 数据加载器
* @param cacheExpireTime 缓存过期时间(秒)
* @param <T> 数据类型
* @return 缓存数据
*/
public <T> T getWithProtection(String key, DataLoader<T> dataLoader, long cacheExpireTime) {
String cacheKey = CACHE_PREFIX + key;
// 1. 尝试从缓存获取
T cachedData = getFromCache(cacheKey);
if (cachedData != null) {
log.debug("Cache hit: {}", key);
return cachedData;
}
// 2. 缓存未命中,尝试获取互斥锁
String lockKey = LOCK_PREFIX + key;
boolean lockAcquired = distributedLockService.acquireLock(lockKey, DEFAULT_LOCK_EXPIRE_TIME, DEFAULT_LOCK_WAIT_TIME);
if (lockAcquired) {
// 3. 获取锁成功,负责重建缓存
try {
// 双重检查,防止其他线程已经重建了缓存
cachedData = getFromCache(cacheKey);
if (cachedData != null) {
log.debug("Cache hit after acquiring lock: {}", key);
return cachedData;
}
// 回源数据库加载数据
log.info("Loading data from database: {}", key);
T data = dataLoader.load();
if (data != null) {
// 写入缓存
setToCache(cacheKey, data, cacheExpireTime);
log.info("Cache rebuilt: {}", key);
}
return data;
} finally {
// 释放锁
distributedLockService.releaseLock(lockKey);
}
} else {
// 4. 获取锁失败,等待并重试
log.info("Failed to acquire lock, waiting for cache rebuild: {}", key);
return waitForCacheRebuild(cacheKey, dataLoader, cacheExpireTime);
}
}
/**
* 从缓存获取数据
*/
@SuppressWarnings("unchecked")
private <T> T getFromCache(String key) {
try {
return (T) redisTemplate.opsForValue().get(key);
} catch (Exception e) {
log.error("Error getting from cache: {}", key, e);
return null;
}
}
/**
* 写入缓存
*/
private <T> void setToCache(String key, T data, long expireTime) {
try {
redisTemplate.opsForValue().set(key, data, expireTime, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("Error setting to cache: {}", key, e);
}
}
/**
* 等待缓存重建完成
*/
private <T> T waitForCacheRebuild(String cacheKey, DataLoader<T> dataLoader, long cacheExpireTime) {
// 短暂等待后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 重试从缓存获取
T cachedData = getFromCache(cacheKey);
if (cachedData != null) {
log.debug("Cache hit after waiting: {}", cacheKey);
return cachedData;
}
// 如果等待后缓存仍未重建,直接回源数据库
log.warn("Cache not rebuilt after waiting, loading from database: {}", cacheKey);
return dataLoader.load();
}
/**
* 数据加载器接口
*/
@FunctionalInterface
public interface DataLoader<T> {
T load();
}
}
3.3 分布式锁服务
DistributedLockService.java
@Service
@Slf4j
public class DistributedLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String LOCK_VALUE_PREFIX = "lock:";
/**
* 获取分布式锁
* @param lockKey 锁键
* @param expireTime 锁过期时间(毫秒)
* @param waitTime 获取锁等待时间(毫秒)
* @return 是否获取成功
*/
public boolean acquireLock(String lockKey, long expireTime, long waitTime) {
String lockValue = LOCK_VALUE_PREFIX + UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
try {
while (System.currentTimeMillis() - startTime < waitTime) {
// 使用 SETNX 命令获取锁
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(success)) {
log.debug("Acquired distributed lock: {}", lockKey);
return true;
}
// 短暂休眠后重试
Thread.sleep(50);
}
log.debug("Failed to acquire distributed lock: {} after {}ms", lockKey, waitTime);
return false;
} catch (Exception e) {
log.error("Error acquiring distributed lock: {}", lockKey, e);
return false;
}
}
/**
* 释放分布式锁
* @param lockKey 锁键
*/
public void releaseLock(String lockKey) {
try {
redisTemplate.delete(lockKey);
log.debug("Released distributed lock: {}", lockKey);
} catch (Exception e) {
log.error("Error releasing distributed lock: {}", lockKey, e);
}
}
/**
* 检查锁是否存在
* @param lockKey 锁键
* @return 是否存在
*/
public boolean isLockExists(String lockKey) {
try {
return redisTemplate.hasKey(lockKey);
} catch (Exception e) {
log.error("Error checking lock existence: {}", lockKey, e);
return false;
}
}
}
3.4 业务服务实现
ProductService.java
@Service
@Slf4j
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private CacheBreakdownProtectionService cacheProtectionService;
private static final long PRODUCT_CACHE_EXPIRE_TIME = 1800; // 商品缓存过期时间 30 分钟
/**
* 获取商品信息(带缓存击穿防护)
* @param productId 商品ID
* @return 商品信息
*/
public Product getProductById(Long productId) {
String cacheKey = "product:" + productId;
return cacheProtectionService.getWithProtection(cacheKey, () -> {
// 回源数据库加载商品信息
log.info("Loading product from database: {}", productId);
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found: " + productId));
// 更新访问时间
product.setLastAccessTime(LocalDateTime.now());
productRepository.save(product);
return product;
}, PRODUCT_CACHE_EXPIRE_TIME);
}
/**
* 获取商品库存(带缓存击穿防护)
* @param productId 商品ID
* @return 库存数量
*/
public Integer getProductStock(Long productId) {
String cacheKey = "product:stock:" + productId;
return cacheProtectionService.getWithProtection(cacheKey, () -> {
// 回源数据库加载库存信息
log.info("Loading product stock from database: {}", productId);
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found: " + productId));
return product.getStock();
}, PRODUCT_CACHE_EXPIRE_TIME);
}
/**
* 更新商品信息
* @param productId 商品ID
* @param product 商品信息
* @return 更新后的商品信息
*/
public Product updateProduct(Long productId, Product product) {
// 更新数据库
Product existingProduct = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found: " + productId));
existingProduct.setName(product.getName());
existingProduct.setPrice(product.getPrice());
existingProduct.setStock(product.getStock());
existingProduct.setUpdateTime(LocalDateTime.now());
Product updatedProduct = productRepository.save(existingProduct);
// 删除缓存,强制下次访问时重建
String cacheKey = "product:" + productId;
deleteCache(cacheKey);
log.info("Product updated and cache deleted: {}", productId);
return updatedProduct;
}
/**
* 删除缓存
*/
private void deleteCache(String key) {
try {
String cacheKey = "cache:" + key;
redisTemplate.delete(cacheKey);
log.debug("Cache deleted: {}", key);
} catch (Exception e) {
log.error("Error deleting cache: {}", key, e);
}
}
@Autowired
private RedisTemplate<String, Object> redisTemplate;
}
3.5 控制器实现
ProductController.java
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
/**
* 获取商品信息
*/
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
try {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
/**
* 获取商品库存
*/
@GetMapping("/{id}/stock")
public ResponseEntity<Integer> getProductStock(@PathVariable Long id) {
try {
Integer stock = productService.getProductStock(id);
return ResponseEntity.ok(stock);
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
/**
* 更新商品信息
*/
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody Product product) {
try {
Product updatedProduct = productService.updateProduct(id, product);
return ResponseEntity.ok(updatedProduct);
} catch (Exception e) {
return ResponseEntity.badRequest().body(null);
}
}
}
3.6 Redis 配置
RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 采用 String 的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash 的 key 也采用 String 的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value 序列化方式采用 jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash 的 value 序列化方式采用 jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
四、生产级实现
4.1 配置文件
application.yml
# 应用配置
spring:
application:
name: cache-breakdown-protection
# Redis 配置
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
database: ${REDIS_DATABASE:0}
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
timeout: 3000ms
# 服务器配置
server:
port: 8080
servlet:
context-path: /
# 缓存击穿防护配置
cache:
breakdown:
protection:
# 默认缓存过期时间(秒)
default-cache-expire-time: 3600
# 默认获取锁等待时间(毫秒)
default-lock-wait-time: 3000
# 默认锁过期时间(毫秒)
default-lock-expire-time: 10000
# 缓存键前缀
cache-key-prefix: cache:
# 锁键前缀
lock-key-prefix: lock:
# 是否启用日志
enable-logging: true
# 监控配置
management:
endpoints:
web:
exposure:
include: "health,info,metrics,prometheus"
endpoint:
health:
show-details: always
# 日志配置
logging:
level:
com.example.cache: info
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
4.2 性能优化
1. 连接池优化
spring:
redis:
lettuce:
pool:
max-active: 20 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 3000ms # 最大等待时间
2. 序列化优化
- 使用 Protobuf 或 Kryo 替代 Jackson
- 减少序列化后的数据大小
- 提高序列化和反序列化速度
3. 缓存预热
- 系统启动时预热热点数据
- 使用定时任务刷新热点数据
- 避免系统启动时的缓存击穿
4.3 监控与告警
1. 监控指标
- 缓存命中率
- 缓存击穿次数
- 锁获取失败次数
- 数据库访问次数
2. 告警配置
# Prometheus 告警规则
groups:
- name: cache-breakdown-alerts
rules:
- alert: CacheBreakdownHigh
expr: cache_breakdown_count > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Cache breakdown rate is high"
description: "Cache breakdown count: {{ $value }}"
3. Grafana 仪表盘
创建 Grafana 仪表盘,展示:
- 缓存命中率趋势
- 缓存击穿次数
- 数据库访问次数
- 系统响应时间
4.4 安全配置
1. Redis 认证
spring:
redis:
password: ${REDIS_PASSWORD}
2. 网络隔离
- 使用 Redis 的 ACL 功能
- 限制 Redis 的访问 IP
- 使用 SSL/TLS 加密连接
3. 数据加密
- 敏感数据加密后存储
- 使用 Redis 的加密模块
- 定期更换加密密钥
五、最佳实践
5.1 缓存设计最佳实践
1. 缓存键设计
- 使用有意义的键名
- 包含业务前缀
- 避免键名冲突
2. 缓存过期时间
- 根据数据更新频率设置
- 热点数据设置较长过期时间
- 冷数据设置较短过期时间
3. 缓存数据结构
- 选择合适的数据结构
- 避免存储过大的对象
- 考虑使用 Hash 结构存储复杂对象
5.2 互斥锁最佳实践
1. 锁粒度
- 使用细粒度锁,减少锁竞争
- 避免使用全局锁
- 根据业务需求设计锁粒度
2. 锁超时
- 设置合理的锁超时时间
- 避免锁超时时间过长
- 考虑业务执行时间
3. 锁释放
- 始终在 finally 块中释放锁
- 避免死锁
- 处理锁释放失败的情况
5.3 性能优化最佳实践
1. 缓存预热
- 系统启动时预热热点数据
- 使用定时任务刷新热点数据
- 避免系统启动时的缓存击穿
2. 批量操作
- 使用 Pipeline 减少网络开销
- 批量获取和设置缓存
- 减少与 Redis 的交互次数
3. 连接池管理
- 合理配置连接池大小
- 监控连接池使用情况
- 及时释放空闲连接
六、案例分析
6.1 案例一:电商商品详情
场景:
- 电商平台商品详情页访问量巨大
- 商品信息缓存过期时,大量请求同时访问数据库
- 数据库压力骤增,导致响应变慢
解决方案:
- 实现缓存击穿防护机制
- 使用互斥锁确保只有一个线程回源数据库
- 其他线程等待或使用旧数据
效果:
- 数据库压力显著降低
- 响应时间保持稳定
- 系统可用性提高
6.2 案例二:秒杀活动
场景:
- 秒杀活动开始时,大量用户同时访问商品信息
- 商品库存缓存过期,导致大量请求访问数据库
- 数据库无法承受高并发,导致活动失败
解决方案:
- 预热秒杀商品缓存
- 实现缓存击穿防护
- 使用互斥锁确保库存更新的原子性
效果:
- 秒杀活动顺利进行
- 数据库压力可控
- 用户体验良好
6.3 案例三:新闻热点
场景:
- 新闻热点文章被大量用户访问
- 文章缓存过期时,大量请求同时访问数据库
- 数据库响应变慢,影响用户体验
解决方案:
- 识别热点文章
- 设置较长的缓存过期时间
- 实现缓存击穿防护
效果:
- 热点文章访问流畅
- 数据库压力可控
- 用户体验提升
七、未来发展趋势
7.1 技术演进
1. 智能缓存管理
- 基于 AI 的缓存策略优化
- 自动识别热点数据
- 动态调整缓存参数
2. 多级缓存
- 本地缓存 + 分布式缓存
- 缓存层级自动切换
- 缓存一致性保证
3. 云原生支持
- 容器化部署
- 服务网格集成
- 云平台原生缓存服务
7.2 应用扩展
1. 跨数据中心缓存
- 多数据中心缓存同步
- 缓存一致性保证
- 故障自动切换
2. 边缘缓存
- 边缘节点的缓存部署
- 就近访问热点数据
- 降低网络延迟
3. 智能预加载
- 基于用户行为的预加载
- 预测热点数据
- 提前加载到缓存
7.3 行业应用
1. 金融行业
- 高可靠性缓存
- 严格的数据一致性
- 合规性和审计要求
2. 电商行业
- 高并发缓存
- 热点数据识别
- 秒杀活动支持
3. 内容行业
- 内容分发网络
- 热点内容缓存
- 用户体验优化
小结
本文介绍了 SpringBoot 应用中实现 Redis 缓存击穿防护和互斥重建的完整解决方案,包括:
- 缓存击穿概念:理解缓存击穿的原理和危害
- 互斥重建机制:确保只有一个线程回源数据库
- 核心实现:缓存击穿防护服务、分布式锁服务、业务服务
- 生产级配置:性能优化、监控告警、安全配置
- 案例分析:电商商品、秒杀活动、新闻热点
- 最佳实践:缓存设计、互斥锁、性能优化
- 未来趋势:智能化、多级缓存、云原生支持
通过实施这些技术方案,您可以建立一个高性能、高可用的缓存系统,防止缓存击穿导致的数据库崩溃,确保系统在高并发场景下的稳定运行。
互动话题
- 您在项目中遇到过哪些缓存击穿的挑战?是如何解决的?
- 您对本文介绍的缓存击穿防护方案有什么改进建议?
- 您认为在微服务架构中,缓存击穿防护有哪些新的挑战?
- 您对未来缓存管理技术的发展有什么看法?
欢迎在评论区分享您的经验和看法!
标题:SpringBoot + Redis 缓存击穿防护 + 互斥重建:热点 Key 过期时,仅一个线程回源 DB
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/13/1773120113062.html
公众号:服务端技术精选
- 导语
- 一、缓存击穿的概念与危害
- 1.1 什么是缓存击穿
- 1.2 缓存击穿与相关概念的区别
- 1.3 缓存击穿的危害
- 二、技术方案设计
- 2.1 架构设计
- 2.2 核心组件
- 2.3 技术选型
- 三、核心实现
- 3.1 依赖配置
- 3.2 缓存击穿防护服务
- 3.3 分布式锁服务
- 3.4 业务服务实现
- 3.5 控制器实现
- 3.6 Redis 配置
- 四、生产级实现
- 4.1 配置文件
- 4.2 性能优化
- 4.3 监控与告警
- 4.4 安全配置
- 五、最佳实践
- 5.1 缓存设计最佳实践
- 5.2 互斥锁最佳实践
- 5.3 性能优化最佳实践
- 六、案例分析
- 6.1 案例一:电商商品详情
- 6.2 案例二:秒杀活动
- 6.3 案例三:新闻热点
- 七、未来发展趋势
- 7.1 技术演进
- 7.2 应用扩展
- 7.3 行业应用
- 小结
- 互动话题
评论