从 Spring Boot 3.5 集成到高并发场景下的缓存三大杀手,一篇讲透
缓存是互联网系统的“银弹”吗?几乎每个 Java 后端开发者都听过这句话:“缓存能解决 80% 的性能问题。”但很少有人告诉你,另外 20% 的问题正是由缓存本身引发的——雪崩、穿透、击穿,这三个词足以让任何一个高并发系统在瞬间崩溃。
2026 年,缓存已经不再是“把数据放进去、取出来”那么简单。随着微服务架构的普及和业务规模的膨胀,分布式缓存已经成为系统稳定性的核心命脉。Spring Boot 3.5 对 Spring Cache 抽象层做了进一步优化,支持了更细粒度的缓存控制和响应式缓存集成,但底层的挑战从未改变。
本文将从 Spring Boot 3.5 集成 Redis 开始,带你一步步构建生产级缓存系统,然后深入剖析缓存雪崩、穿透、击穿的成因与解决方案,让你在面对高并发冲击时心中有数。
一、Spring Cache 抽象:统一门面背后的力量
1.1 为什么需要缓存抽象层?
在没有 Spring Cache 之前,我们的代码大概是这样的:
@Service
public class ProductService {
private final RedisTemplate<String, Object> redisTemplate;
private final ProductRepository productRepository;
public Product getProduct(Long id) {
// 1. 先查缓存
String key = "product:" + id;
Product cached = (Product) redisTemplate.opsForValue().get(key);
if (cached != null) {
return cached;
}
// 2. 缓存未命中,查数据库
Product product = productRepository.findById(id).orElse(null);
// 3. 写入缓存
if (product != null) {
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
}
return product;
}
}
这个方法在项目里出现几十上百次后,你会发现自己写了一大堆重复代码,而且每个方法的缓存逻辑还不一致——有的设 TTL,有的不设;有的在更新时清缓存,有的直接删 Key。代码的“坏味道”越来越重。
Spring Cache 抽象层通过 AOP 和注解的方式,将缓存的读、写、删除等操作从业务代码中剥离出来,统一由框架管理。你只需要在方法上添加 @Cacheable、@CachePut 或 @CacheEvict 注解即可。
1.2 核心注解解析
| 注解 | 作用 | 执行时机 |
|---|---|---|
@Cacheable | 触发缓存读取 | 方法执行前 |
@CachePut | 强制更新缓存(不影响方法执行) | 方法执行后 |
@CacheEvict | 触发缓存删除 | 方法执行后 |
@Caching | 组合多个缓存操作 | 方法执行前后 |
@CacheConfig | 类级别共享缓存配置 | — |
1.3 Spring Cache + Redis 的架构分层

二、实战:Spring Boot 3.5 集成 Redis 缓存
2.1 项目依赖配置
在 pom.xml 中添加以下依赖:
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Cache(提供 @Cacheable 等注解) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Spring Data Redis(Lettuce 客户端) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 连接池依赖(建议生产环境使用) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 可选:JSON 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2.2 配置文件 application.yml
spring:
data:
redis:
host: localhost
port: 6379
password: # 若有密码则配置
database: 0
lettuce:
pool:
max-active: 20
max-idle: 8
min-idle: 2
max-wait: -1ms
shutdown-timeout: 200ms
cache:
type: redis
redis:
# 全局缓存过期时间(毫秒),默认永不过期
time-to-live: 600000 # 10 分钟
# 是否允许缓存空值(防止缓存穿透)
cache-null-values: true
# Key 前缀
key-prefix: "cache:"
# 是否使用 Key 前缀
use-key-prefix: true
2.3 Redis 配置类(自定义序列化)
package com.example.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Redis 缓存配置类
* 启用 Spring Cache 并自定义序列化方式
*/
@Configuration
@EnableCaching // 启用 Spring Cache 注解支持
public class RedisCacheConfig {
/**
* 配置 RedisCacheManager
* 使用 GenericJackson2JsonRedisSerializer 实现 JSON 序列化
* 这样可以存储任意类型的对象,且可读性更好
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 创建 ObjectMapper 用于 JSON 序列化
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
// 配置默认的缓存策略
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// Key 序列化使用 String(方便查看)
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
)
)
// Value 序列化使用 JSON(可读性好,且支持类型信息)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(serializer)
)
// 默认过期时间 10 分钟
.entryTtl(Duration.ofMinutes(10))
// 允许缓存 null 值(防止穿透)
.disableCachingNullValues(); // 生产环境建议关闭,或改为 .enableCachingNullValues()
// 构建 CacheManager
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware() // 支持事务
.build();
}
}
关键点:使用
GenericJackson2JsonRedisSerializer时,会自动在 JSON 中保存类型信息(@class字段),反序列化时不需要额外指定类型。这对缓存多种类型的对象非常有用。
2.4 实战:带缓存的服务层代码
package com.example.service;
import com.example.entity.Product;
import com.example.repository.ProductRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
/**
* 根据 ID 查询商品
* 缓存 Key 规则:product:{id}
* 缓存过期时间由全局配置决定(10 分钟)
*
* 注意:sync 参数在 Spring Boot 3.5 中已全面支持
* 高并发场景下,同一时间只有一个线程去加载缓存,其他线程等待
*/
@Cacheable(value = "product", key = "#id", sync = true)
public Product getProductById(Long id) {
log.info("缓存未命中,从数据库查询商品: id={}", id);
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("商品不存在: " + id));
}
/**
* 更新商品信息,同时更新缓存
* @CachePut 无论是否命中缓存,都会执行方法并将结果写入缓存
*/
@CachePut(value = "product", key = "#product.id")
@Transactional
public Product updateProduct(Product product) {
log.info("更新商品并刷新缓存: id={}", product.getId());
Product existing = productRepository.findById(product.getId())
.orElseThrow(() -> new RuntimeException("商品不存在: " + product.getId()));
existing.setName(product.getName());
existing.setPrice(product.getPrice());
existing.setStock(product.getStock());
return productRepository.save(existing);
}
/**
* 删除商品并清除缓存
* @CacheEvict 会删除指定的缓存条目
* allEntries = true 会清空该缓存分区下的所有 Key
*/
@CacheEvict(value = "product", key = "#id", beforeInvocation = false)
@Transactional
public void deleteProduct(Long id) {
log.info("删除商品并清除缓存: id={}", id);
productRepository.deleteById(id);
}
/**
* 批量清除商品缓存(例如库存批量更新后)
* 使用 @Caching 组合多个缓存操作
*/
@org.springframework.cache.annotation.Caching(evict = {
@CacheEvict(value = "product", key = "#id"),
@CacheEvict(value = "productList", allEntries = true)
})
public void clearProductCache(Long id) {
log.info("清除商品相关缓存: id={}", id);
// 通常不需要额外逻辑,仅用于触发缓存清除
}
}
三、缓存三大杀手:雪崩、穿透、击穿
这是缓存系统最经典的三大难题。理解它们,是构建高可用缓存系统的第一步。
3.1 缓存雪崩(Cache Avalanche)
定义:大量的缓存 Key 在同一时刻集体失效,或者缓存服务整个不可用,导致所有请求直接打到数据库,瞬间压垮数据库。
常见场景:
- 缓存设置了相同的过期时间(例如全部 30 分钟),到了整点集体失效
- Redis 服务宕机或网络分区
- 大量 Key 在同一个时间窗口被逐出(内存不足触发 LRU 淘汰)
解决方案:
| 策略 | 说明 | 实现方式 |
|---|---|---|
| 随机过期时间 | 给 TTL 增加随机偏移量,避免同时失效 | expire = 600 + random(0, 300) |
| Redis 高可用 | 部署 Redis Cluster 或 Sentinel 集群 | 主从 + 哨兵 / 集群模式 |
| 本地缓存兜底 | 在应用内存中缓存热点数据,Redis 挂了仍能支撑 | Caffeine 缓存 |
| 限流降级 | 当缓存不可用时,限制对数据库的并发请求 | Sentinel / Resilience4j |
Spring Cache 中实现随机 TTL:
@Bean
public RedisCacheManagerBuilderCustomizer cacheManagerBuilderCustomizer() {
return builder -> {
// 为不同缓存分区设置不同的过期时间
builder.withCacheConfiguration("product",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10 + new Random().nextInt(5)))
);
builder.withCacheConfiguration("productList",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5 + new Random().nextInt(3)))
);
};
}
3.2 缓存穿透(Cache Penetration)
定义:查询一个根本不存在的数据(例如查询 ID = -1 的用户),缓存没有命中,数据库也没有查到,每次请求都会穿透缓存直达数据库。如果这种请求量很大(例如恶意攻击),数据库会被瞬间打满。
常见场景:
- 查询不存在的数据,如
GET /api/user/-1 - 查询超长 ID 或随机字符串,数据库无论如何都查不到
- 爬虫或攻击者构造大量不存在的 Key 刷接口
解决方案:

| 策略 | 说明 | 适用场景 |
|---|---|---|
| 缓存空对象 | 将 null 也缓存起来,TTL 设置较短(如 5 分钟) | 容易实现,但会消耗少量内存 |
| 布隆过滤器(Bloom Filter) | 预先将所有存在的 Key 存入布隆过滤器,不存在则直接拦截 | 内存高效,但有极低的误判率 |
| 请求参数校验 | 在网关层校验参数合法性(如 ID 不能为负数) | 最基础的防护 |
缓存空对象的实现(Spring Cache 默认支持):
spring:
cache:
redis:
cache-null-values: true # 允许缓存 null
@Service
public class UserService {
@Cacheable(value = "user", key = "#id")
public User getUser(Long id) {
// 如果数据库查不到,返回 null,Spring Cache 会缓存一个空对象
// 但需要注意 TTL 应该比正常数据短
return userRepository.findById(id).orElse(null);
}
}
布隆过滤器实现(Google Guava):
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.3.0-jre</version>
</dependency>
@Component
public class BloomFilterService {
private final BloomFilter<Long> bloomFilter;
@PostConstruct
public void init() {
// 创建布隆过滤器:预计 100 万数据,误判率 1%
bloomFilter = BloomFilter.create(
Funnels.longFunnel(),
1_000_000,
0.01
);
// 从数据库加载所有存在的用户 ID 到布隆过滤器
List<Long> userIds = userRepository.findAllIds();
userIds.forEach(bloomFilter::put);
}
public boolean likelyExists(Long id) {
return bloomFilter.mightContain(id);
}
}
3.3 缓存击穿(Cache Breakdown)
定义:某个热点 Key 在缓存过期的瞬间,恰好有大量并发请求同时查询该 Key。这些请求同时发现缓存未命中,全部去数据库查询,导致数据库压力暴增。
常见场景:
- 热门商品详情页(如秒杀商品),缓存过期瞬间
- 热点新闻详情页,缓存过期瞬间
- 明星/大 V 的用户信息页
解决方案:
| 策略 | 说明 | 实现 |
|---|---|---|
| 互斥锁(Mutex) | 只允许一个线程去加载数据,其他线程等待 | Redis 分布式锁 |
| 永不过期(逻辑过期) | 缓存不设 TTL,由后台任务异步刷新 | 定时任务 + 双重检查 |
| 提前预热 | 在缓存过期前主动刷新(如通过消息通知) | 定时任务 |
互斥锁方案(推荐):
@Service
@Slf4j
public class ProductServiceWithLock {
private final StringRedisTemplate stringRedisTemplate;
private final ProductRepository productRepository;
/**
* 使用 Redis 分布式锁防止缓存击穿
*
* 核心思路:
* 1. 先查缓存,命中则直接返回
* 2. 缓存未命中,尝试获取分布式锁
* 3. 获取锁的线程负责加载数据并写入缓存
* 4. 未获取锁的线程等待,然后重新从缓存获取
*/
public Product getProductWithLock(Long id) {
String cacheKey = "product:" + id;
String lockKey = "lock:product:" + id;
// 1. 先查缓存
Product cached = getFromCache(cacheKey);
if (cached != null) {
return cached;
}
// 2. 尝试获取分布式锁(使用 Redisson 或 Redis SETNX)
String lockValue = UUID.randomUUID().toString();
Boolean acquired = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(5));
if (Boolean.TRUE.equals(acquired)) {
try {
// 3. 双重检查:获得锁后再次检查缓存(避免重复加载)
Product cachedAgain = getFromCache(cacheKey);
if (cachedAgain != null) {
return cachedAgain;
}
// 4. 从数据库加载数据
log.info("获取到锁,从数据库加载数据: id={}", id);
Product product = productRepository.findById(id).orElse(null);
// 5. 写入缓存
if (product != null) {
stringRedisTemplate.opsForValue()
.set(cacheKey, JSON.toJSONString(product),
Duration.ofMinutes(10));
} else {
// 缓存空值,防止穿透
stringRedisTemplate.opsForValue()
.set(cacheKey, "", Duration.ofMinutes(1));
}
return product;
} finally {
// 6. 释放锁(使用 Lua 脚本保证原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
stringRedisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
} else {
// 7. 未获取到锁,短暂等待后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 递归调用,重新尝试获取缓存
return getProductWithLock(id);
}
}
}
3.4 三大问题对比速查表
| 问题 | 成因 | 核心区别 | 解决方案 |
|---|---|---|---|
| 缓存雪崩 | 大量 Key 同时失效 | 量大面广 | 随机 TTL + Redis 高可用 + 本地缓存 |
| 缓存穿透 | 查询不存在的数据 | 无效数据 | 布隆过滤器 + 缓存空值 + 参数校验 |
| 缓存击穿 | 热点 Key 失效瞬间并发 | 单个热点 | 互斥锁 + 永不过期 + 提前预热 |

四、生产环境最佳实践清单
| 实践项 | 说明 | 优先级 |
|---|---|---|
| 缓存监控 | 监控 Redis 命中率、QPS、内存使用率,设置告警阈值 | ⭐⭐⭐ |
| Key 命名规范 | 使用业务前缀 + 版本号,如 v1:product:{id},便于管理 | ⭐⭐⭐ |
| 序列化选型 | 生产环境推荐 Jackson2JsonRedisSerializer 或 Protobuf,避免 JdkSerialization(体积大、不安全) | ⭐⭐⭐ |
| 连接池配置 | 合理设置 max-active 和 max-idle,避免连接耗尽 | ⭐⭐ |
| 缓存热加载 | 系统启动时预热热点数据到缓存中(如首页数据、热门商品) | ⭐⭐ |
| 缓存降级策略 | 当 Redis 不可用时,返回兜底数据或静态页面,而非抛出异常 | ⭐⭐ |
| 缓存版本管理 | 数据结构变更时,使用新的 Key 前缀,实现平滑迁移 | ⭐ |
五、总结
缓存是互联网系统性能优化的利器,但也是一把双刃剑。用好缓存,系统能轻松扛住百万 QPS;用不好缓存,可能一个简单的缓存过期就能把数据库打挂。
本文从 Spring Boot 3.5 的 Spring Cache 抽象层入手,详细介绍了 @Cacheable、@CachePut、@CacheEvict 等注解的实战用法,以及 Redis 缓存管理器的自定义配置。更重要的是,我们深入剖析了缓存三大杀手——雪崩、穿透、击穿的成因,并提供了互斥锁、布隆过滤器、随机 TTL 等经过生产验证的解决方案。
2026 年的今天,Redis 和 Spring Cache 的组合已经成为 Java 后端开发的标配。但工具只是手段,对缓存原理的理解和对各种异常场景的预判,才是真正区分普通开发者和优秀架构师的分水岭。
系列拓展阅读
- 《WSL2 + Docker Desktop:Windows 下的完美 Java 开发环境》 —— Redis 本地开发的基础环境
- 《Spring Boot 3.4 Docker 镜像最佳实践(含分层构建)》 —— 缓存 + 容器的组合拳
- 《Java 应用接入 Prometheus + Grafana 全记录》 —— Redis 缓存监控的配套方案
- 《Arthas 与火焰图:Java 生产环境在线诊断从入门到精通》 —— 缓存问题的在线诊断利器
参考文献
- Spring Data Redis Reference Documentation. https://docs.spring.io/spring-data/redis/docs/current/reference/html/
- Spring Cache Abstraction Documentation. https://docs.spring.io/spring-framework/reference/integration/cache.html
- Redis Official Documentation. “Redis as a Cache.” https://redis.io/docs/management/scalability/
- “Redis 缓存雪崩、穿透、击穿解决方案全解析.” 阿里云开发者社区.
- “Cache-Aside Pattern: Practical Guide to Distributed Caching.” Baeldung.







