Spring Cache面试题
2026/1/15大约 5 分钟SpringSpring CacheJava缓存后端
Spring Cache 面试题
基础概念
Q1: Spring Cache 是什么?
Spring Cache 是 Spring 框架提供的缓存抽象层,它定义了一套统一的缓存操作接口和注解,使开发者可以用声明式的方式操作缓存,而不依赖于具体的缓存实现。
核心特点:
- 声明式缓存:通过注解实现缓存操作
- 与实现解耦:支持多种缓存实现(Caffeine、Redis、Ehcache 等)
- 基于 AOP:通过代理拦截方法调用
Q2: Spring Cache 的核心注解有哪些?
| 注解 | 作用 |
|---|---|
@EnableCaching | 启用缓存功能 |
@Cacheable | 查询缓存,不存在则执行方法并缓存 |
@CachePut | 执行方法并更新缓存 |
@CacheEvict | 删除缓存 |
@Caching | 组合多个缓存操作 |
@CacheConfig | 类级别的缓存配置 |
Q3: @Cacheable 和 @CachePut 的区别?
@Cacheable:先查缓存,命中则直接返回,不执行方法@CachePut:始终执行方法,并将结果更新到缓存
使用场景:
@Cacheable:查询操作@CachePut:更新操作,需要同步更新缓存
Q4: condition 和 unless 的区别?
@Cacheable(
value = "users",
condition = "#id > 0", // 方法执行前判断
unless = "#result == null" // 方法执行后判断
)
public User getById(Long id) {
return userRepository.findById(id).orElse(null);
}| 属性 | 判断时机 | 作用 |
|---|---|---|
| condition | 方法执行前 | 决定是否查询/使用缓存 |
| unless | 方法执行后 | 决定是否将结果存入缓存 |
原理机制
Q5: Spring Cache 的工作原理?
Spring Cache 基于 AOP 实现:
@EnableCaching导入CachingConfigurationSelector- 注册
CacheInterceptor拦截器 - 方法调用时,拦截器根据注解执行缓存逻辑
// 简化的执行流程
public Object invoke(MethodInvocation invocation) {
// 1. 解析缓存注解
CacheOperationContext context = getCacheOperationContext(invocation);
// 2. 处理 @CacheEvict(beforeInvocation=true)
processCacheEvicts(context, true);
// 3. 处理 @Cacheable - 查询缓存
Cache.ValueWrapper cached = findCachedValue(context);
if (cached != null) {
return cached.get();
}
// 4. 执行目标方法
Object result = invocation.proceed();
// 5. 处理 @CachePut - 更新缓存
processCachePuts(context, result);
// 6. 处理 @CacheEvict(beforeInvocation=false)
processCacheEvicts(context, false);
return result;
}Q6: 为什么同一个类中方法调用缓存不生效?
Spring Cache 基于 AOP 代理实现,同类方法调用不经过代理。
@Service
public class UserService {
@Cacheable("users")
public User getById(Long id) {
return userRepository.findById(id).orElse(null);
}
public User getByIdWrapper(Long id) {
// 同类调用,不走代理,缓存不生效
return this.getById(id);
}
}解决方案:
// 方案1:注入自身
@Service
public class UserService {
@Autowired
private UserService self;
public User getByIdWrapper(Long id) {
return self.getById(id); // 通过代理调用
}
}
// 方案2:使用 AopContext
@Service
public class UserService {
public User getByIdWrapper(Long id) {
return ((UserService) AopContext.currentProxy()).getById(id);
}
}Q7: Spring Cache 如何生成默认 Key?
默认使用 SimpleKeyGenerator:
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
}规则:
- 无参数:返回
SimpleKey.EMPTY - 单参数:返回参数本身
- 多参数:返回
SimpleKey对象
实践问题
Q8: 如何解决缓存穿透?
缓存穿透:查询不存在的数据,每次都穿透到数据库。
// 方案1:缓存空值
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 改为:缓存空对象
@Cacheable(value = "users", key = "#id")
public User getById(Long id) {
User user = userRepository.findById(id).orElse(null);
return user != null ? user : User.NULL_USER; // 返回空对象
}
// 方案2:布隆过滤器
@Cacheable(value = "users", key = "#id", condition = "@bloomFilter.mightContain(#id)")
public User getById(Long id) {
return userRepository.findById(id).orElse(null);
}Q9: 如何解决缓存击穿?
缓存击穿:热点 Key 过期瞬间,大量请求穿透到数据库。
// 方案1:使用 sync 属性
@Cacheable(value = "users", key = "#id", sync = true)
public User getById(Long id) {
return userRepository.findById(id).orElse(null);
}
// sync=true 时,只有一个线程执行方法,其他线程等待Q10: 如何解决缓存雪崩?
缓存雪崩:大量 Key 同时过期,导致数据库压力骤增。
// 方案:随机过期时间
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.expireAfter(new Expiry<Object, Object>() {
@Override
public long expireAfterCreate(Object key, Object value, long currentTime) {
// 基础时间 + 随机时间
return TimeUnit.MINUTES.toNanos(10 + new Random().nextInt(5));
}
// ... 其他方法
}));
return manager;
}
}Q11: 多级缓存如何保证一致性?
通过消息机制(Redis Pub/Sub、MQ)通知所有节点清除本地缓存。
Q12: @Cacheable 的 key 如何设计?
// 1. 简单 Key
@Cacheable(value = "users", key = "#id")
// 2. 组合 Key
@Cacheable(value = "users", key = "#type + ':' + #id")
// 3. 对象属性
@Cacheable(value = "users", key = "#query.type + ':' + #query.id")
// 4. 自定义 KeyGenerator
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")Key 设计原则:
- 唯一性:确保不同数据对应不同 Key
- 可读性:便于排查问题
- 简洁性:避免 Key 过长
高级特性
Q13: 如何实现缓存预热?
@Component
public class CacheWarmer implements ApplicationRunner {
@Autowired
private UserService userService;
@Override
public void run(ApplicationArguments args) {
// 启动时预加载热点数据
List<Long> hotIds = getHotUserIds();
hotIds.parallelStream().forEach(userService::getById);
}
}Q14: 如何监控缓存命中率?
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10000)
.recordStats()); // 开启统计
return manager;
}
}
// 获取统计信息
@RestController
public class CacheStatsController {
@Autowired
private CacheManager cacheManager;
@GetMapping("/cache/stats")
public Map<String, Object> stats() {
CaffeineCache cache = (CaffeineCache) cacheManager.getCache("users");
CacheStats stats = cache.getNativeCache().stats();
Map<String, Object> result = new HashMap<>();
result.put("hitRate", stats.hitRate());
result.put("hitCount", stats.hitCount());
result.put("missCount", stats.missCount());
return result;
}
}Q15: Spring Cache 支持哪些缓存实现?
| 实现 | 说明 | 适用场景 |
|---|---|---|
| ConcurrentMapCache | 基于 ConcurrentHashMap | 开发测试 |
| Caffeine | 高性能本地缓存 | 单机高性能 |
| Redis | 分布式缓存 | 集群环境 |
| Ehcache | 企业级缓存 | 复杂缓存策略 |
| Hazelcast | 分布式内存网格 | 分布式计算 |
小结
Spring Cache 面试重点:
- 核心注解的使用和区别
- 基于 AOP 的实现原理
- 缓存穿透、击穿、雪崩的解决方案
- 多级缓存的一致性问题
- Key 设计和缓存监控
面试题预览
高频面试题
- Spring Cache 的实现原理是什么?
- 同类方法调用缓存为什么不生效?
- 如何解决缓存穿透、击穿、雪崩?
- 多级缓存如何保证一致性?
