响应时间动辄几秒、数据库连接池告警不断、服务器 CPU 使用率飙升到 90%——这些性能问题你是否也遇到过?在高并发业务场景中,如何避免重复计算和频繁数据库访问是开发人员面临的常见挑战。Spring 框架的缓存机制通过简单的注解配置,就能在不改变现有代码结构的前提下,大幅提升系统响应速度和吞吐量。
Spring 缓存注解基础
Spring 自 3.1 版本开始引入缓存抽象层,开发者只需通过添加注解就能实现方法级别的缓存功能。这套机制基于 AOP 实现,对原有业务代码几乎零侵入,而且支持多种缓存存储方案,从简单的本地缓存到分布式缓存如 Redis 都能无缝集成。
缓存工作流程图:
graph TD
A[客户端请求] --> B{缓存中是否存在?}
B -->|是| C[返回缓存数据]
B -->|否| D[执行方法]
D --> E[存入缓存]
E --> F[返回结果]
@Cacheable 注解详解
基本作用
@Cacheable 注解标记的方法,其返回值会被自动缓存。当首次调用该方法时,方法会被执行并将结果存入缓存;后续使用相同参数调用时,Spring 会直接从缓存返回结果,不再执行方法内部逻辑。
关键参数
value/cacheNames:指定缓存名称(必填)key:缓存的键,支持 SpEL 表达式定制condition:方法执行前判断的缓存条件unless:方法执行后判断的否定缓存条件keyGenerator:自定义键生成器,与key互斥cacheManager:自定义缓存管理器sync:是否使用同步模式,防止缓存击穿
基本使用示例
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) {
// 模拟数据库查询
System.out.println("从数据库查询用户: " + id);
return userRepository.findById(id).orElse(null);
}
}
当你多次调用getUserById(1L)方法时,只有第一次会打印"从数据库查询用户: 1",后续调用直接返回缓存结果,提高响应速度。
SpEL 表达式与参数引用
当方法有多个参数时,使用 SpEL 表达式引用参数:
// 单参数方法,可直接使用参数名
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 多参数方法,建议使用@Param增强可读性
@Cacheable(value = "userCache", key = "#id + ':' + #role")
public List<User> getUsersByRole(@Param("id") Long id, @Param("role") String role) {
return userRepository.findByIdAndRole(id, role);
}
// 引用参数对象的属性
@Cacheable(value = "userCache", key = "#user.id")
public User updateUserCache(User user) {
return userRepository.findById(user.getId()).orElse(null);
}
condition 与 unless 的区别
这两个参数都用于控制缓存条件,但作用时机不同:
// condition在方法执行前判断
// 若id <= 0,则不走缓存,直接执行方法
@Cacheable(value = "userCache", key = "#id", condition = "#id > 0")
public User getUserById(Long id) {
System.out.println("只有id > 0时才走缓存逻辑");
return userRepository.findById(id).orElse(null);
}
// unless在方法执行后判断
// 方法一定会执行,但结果为null时不会被缓存
@Cacheable(value = "userCache", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
System.out.println("方法执行了,但null结果不会缓存");
return userRepository.findById(id).orElse(null);
}
简单理解:condition决定是否使用缓存,unless决定是否将结果放入缓存。
@CacheEvict 注解详解
基本作用
@CacheEvict 注解标记的方法执行后,会清除指定的缓存数据。当我们更新或删除数据时,通常需要清除相关缓存,确保数据一致性。
关键参数
value/cacheNames:要清除的缓存名称key:要清除的缓存键allEntries:是否清除所有缓存项目beforeInvocation:是否在方法执行前清除缓存
基本使用示例
@Service
public class UserService {
@CacheEvict(value = "userCache", key = "#user.id")
public void updateUser(User user) {
System.out.println("更新用户并清除缓存: " + user.getId());
userRepository.save(user);
}
}
当调用updateUser方法更新用户信息时,对应用户的缓存会被自动清除,保证后续查询能获取到最新数据。
allEntries 参数使用时机
@CacheEvict(value = "userCache", allEntries = true)
public void importUsers(List<User> users) {
System.out.println("批量导入用户,清空所有用户缓存");
userRepository.saveAll(users);
}
allEntries = true会清空整个缓存区域的所有数据。这适用于大批量数据更新场景,但要注意:频繁使用会导致缓存命中率急剧下降,应谨慎使用。
beforeInvocation 参数的重要性
@CacheEvict(value = "userCache", key = "#id", beforeInvocation = true)
public void deleteUser(Long id) {
System.out.println("删除用户: " + id);
userRepository.deleteById(id);
// 如果这里发生异常,缓存也已被清除
if (someCondition) {
throw new RuntimeException("操作失败");
}
}
默认情况下,缓存在方法成功执行后才会被清除。设置beforeInvocation = true可确保无论方法是否抛出异常,缓存都会被清除,这对处理事务回滚时的缓存一致性尤为重要。
@CachePut 注解—更灵活的缓存更新方式
Spring 还提供了@CachePut 注解,用于更新缓存但不影响方法执行。
@CachePut 特点
- 始终执行方法,并用返回值更新缓存
- 不会像@Cacheable 那样先检查缓存
- 适合在更新操作后立即刷新缓存数据
@CachePut(value = "userCache", key = "#user.id")
public User updateAndRefreshCache(User user) {
// 更新用户
User updatedUser = userRepository.save(user);
// 返回值会更新到缓存
return updatedUser;
}
三种缓存注解的区别:
graph TB
A[缓存操作选择] --> B{"需要读取还是更新?"}
B -->|读取| C{"希望跳过方法?"}
C -->|"是,有缓存直接返回"| D["@Cacheable"]
C -->|"否,始终执行方法"| E["@CachePut"]
B -->|清除| F["@CacheEvict"]
注解协同工作流程
缓存注解如何在真实业务流程中工作:
sequenceDiagram
participant 客户端
participant 缓存
participant 服务层
participant 数据库
客户端->>服务层: 请求数据(@Cacheable)
服务层->>缓存: 检查缓存
alt 缓存命中
缓存-->>服务层: 返回缓存数据
服务层-->>客户端: 返回结果
else 缓存未命中
服务层->>数据库: 查询数据库
数据库-->>服务层: 返回数据
服务层->>缓存: 存入缓存
服务层-->>客户端: 返回结果
end
客户端->>服务层: 更新数据
alt 使用@CacheEvict
服务层->>数据库: 更新数据库
服务层->>缓存: 清除相关缓存
else 使用@CachePut
服务层->>数据库: 更新数据库
服务层->>缓存: 更新缓存数据
end
服务层-->>客户端: 返回结果
缓存配置与自定义实现
基础配置
使用缓存注解前,需要启用缓存功能并配置缓存管理器:
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching // 必须添加此注解
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
// 简单的内存缓存实现,仅适用于单体应用
return new ConcurrentMapCacheManager("userCache", "productCache", "stockCache");
}
}
自定义键生成器
默认情况下,Spring 使用方法参数作为缓存键。我们可以自定义键生成逻辑:
@Configuration
public class CacheKeyConfig {
@Bean("methodNameKeyGenerator")
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName());
sb.append(".");
sb.append(method.getName());
sb.append(":");
for (Object param : params) {
if (param != null) {
sb.append(param.toString());
sb.append("-");
} else {
sb.append("null-");
}
}
return sb.toString();
};
}
}
// 使用自定义KeyGenerator
@Cacheable(value = "userCache", keyGenerator = "methodNameKeyGenerator")
public User getUserById(Long id) {
// 缓存键会是: "UserService.getUserById:1-"
return userRepository.findById(id).orElse(null);
}
缓存序列化方式选择
选择合适的序列化方式对缓存性能影响很大:
| 序列化方式 | 性能特点 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|---|
| JDK 原生序列化 | 速度较慢,体积较大 | 原生支持,无需额外依赖 | 仅支持 Java,序列化体积大 | 内部系统临时测试 |
| JSON 序列化 | 中等速度,体积小 | 跨语言,可读性好,体积小 | 类型信息丢失,复杂对象处理困难 | 多语言访问的数据 |
| Protostuff | 极快速度,极小体积 | 性能最佳,体积最小 | 需要额外依赖,跨语言支持复杂 | 对性能要求极高的内部系统 |
Redis 缓存配置
生产环境通常使用 Redis 作为分布式缓存:
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默认缓存配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // 默认1小时过期
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
);
// 不同缓存名称配置不同过期时间,避免缓存雪崩
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("userCache", defaultConfig.entryTtl(
Duration.ofHours(2)
.plusMinutes(new Random().nextInt(60)))); // 随机过期时间
configMap.put("productCache", defaultConfig.entryTtl(Duration.ofHours(4)));
configMap.put("stockCache", defaultConfig.entryTtl(Duration.ofMinutes(30)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configMap)
.build();
}
}
缓存预热实现
对于高频访问的核心数据,可以在系统启动时预先加载到缓存:
@Component
public class CacheWarmer implements CommandLineRunner {
private final ProductService productService;
public CacheWarmer(ProductService productService) {
this.productService = productService;
}
@Override
public void run(String... args) {
System.out.println("开始预热商品缓存...");
// 异步加载热门商品,避免影响系统启动速度
CompletableFuture.runAsync(() -> {
try {
// 等待应用完全启动
Thread.sleep(5000);
// 获取热门商品ID列表(实际项目中可从数据库查询)
List<Long> hotProductIds = Arrays.asList(1L, 2L, 3L, 4L, 5L);
// 调用缓存方法加载数据
hotProductIds.forEach(id -> {
try {
productService.getProduct(id);
System.out.println("预热商品: " + id);
} catch (Exception e) {
System.err.println("预热商品失败: " + id);
}
});
System.out.println("商品缓存预热完成");
} catch (Exception e) {
System.err.println("缓存预热异常: " + e.getMessage());
}
});
}
}
实际应用案例:商品库存管理
下面是一个完整的商品库存管理示例,展示了三个缓存注解的协同使用:
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// 查询商品,结果会被缓存
@Cacheable(value = "productCache", key = "#productId",
condition = "#productId > 0", unless = "#result == null")
public Product getProduct(Long productId) {
System.out.println("从数据库查询商品: " + productId);
try {
return productRepository.findById(productId).orElse(null);
} catch (Exception e) {
// 异常处理,避免缓存异常结果
System.err.println("查询商品异常: " + e.getMessage());
return null;
}
}
// 查询库存,使用sync防止缓存击穿
@Cacheable(value = "stockCache", key = "#productId",
unless = "#result <= 0", sync = true)
public int getProductStock(Long productId) {
System.out.println("从数据库查询库存: " + productId);
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("商品不存在"));
return product.getStock();
}
// 更新库存并刷新缓存
@CachePut(value = "stockCache", key = "#productId")
@CacheEvict(value = "productCache", key = "#productId") // 同时清除商品缓存
@Transactional // 确保事务一致性
public Integer updateStock(Long productId, int newStock) {
System.out.println("更新商品库存: " + productId);
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("商品不存在"));
product.setStock(newStock);
productRepository.save(product);
return newStock; // 返回新库存值,由@CachePut缓存
}
// 下架商品,提前清除相关缓存
@CacheEvict(value = {"productCache", "stockCache"}, key = "#productId",
beforeInvocation = true)
@Transactional
public void removeProduct(Long productId) {
System.out.println("下架商品: " + productId);
productRepository.deleteById(productId);
}
// 批量导入商品,清空缓存
@CacheEvict(value = {"productCache", "stockCache"}, allEntries = true)
@Transactional
public void importProducts(List<Product> products) {
System.out.println("批量导入商品,清空所有缓存");
productRepository.saveAll(products);
}
}
缓存监控与管理
在生产环境中,监控缓存性能对排查问题至关重要:
@Configuration
public class CacheMonitorConfig {
@Bean
public CacheManagerCustomizer<ConcurrentMapCacheManager> cacheManagerCustomizer(MeterRegistry registry) {
return cacheManager -> {
cacheManager.setCacheNames(Arrays.asList("userCache", "productCache", "stockCache"));
cacheManager.setAllowNullValues(false);
// 为每个缓存添加监控指标
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
// 注册缓存命中率、未命中次数等指标
registry.gauge(cacheName + ".size",
cache, c -> ((ConcurrentMapCache)c).getNativeCache().size());
});
};
}
}
通过 Spring Boot Actuator 暴露缓存指标:
# application.properties
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.metrics.tags.application=${spring.application.name}
多级缓存架构
大型系统通常采用多级缓存提高性能和可用性:
graph TD
A[客户端请求] -->|读取| B[本地缓存<br>Caffeine]
B -->|未命中| C[分布式缓存<br>Redis]
C -->|未命中| D[数据库]
D -->|写入| C
C -->|写入| B
E[数据更新] -->|更新| D
E -->|失效| C
E -->|失效| B
linkStyle 5 stroke:#FFD600
linkStyle 6 stroke:#FFD600,fill:none
linkStyle 7 stroke:#FFD600,fill:none
高级应用场景
缓存版本控制
当数据结构变更时,通过版本号避免缓存兼容性问题:
@Cacheable(value = "userCache", key = "#id + '_v2'")
public UserV2 getUserById(Long id) {
// 返回新版本用户对象
UserV2 userV2 = new UserV2();
// 从旧数据转换...
return userV2;
}
异步缓存加载
对于耗时计算,可使用异步加载减少用户等待:
@Cacheable(value = "reportCache", key = "#reportId")
public CompletableFuture<Report> generateReportAsync(String reportId) {
return CompletableFuture.supplyAsync(() -> {
System.out.println("异步生成报表: " + reportId);
// 耗时操作
return reportGenerator.createReport(reportId);
}, asyncExecutor);
}
布隆过滤器防止缓存穿透
@Service
public class UserServiceWithBloomFilter {
private final BloomFilter<Long> userIdFilter;
private final UserRepository userRepository;
public UserServiceWithBloomFilter(UserRepository userRepository) {
this.userRepository = userRepository;
// 创建布隆过滤器(实际应用中可使用Redis布隆过滤器)
this.userIdFilter = BloomFilter.create(
Funnels.longFunnel(),
10000, // 预期元素数量
0.01 // 误判率
);
// 初始化过滤器,加载所有ID(生产环境应该异步加载)
userRepository.findAllIds().forEach(userIdFilter::put);
}
@Cacheable(value = "userCache", key = "#id", condition = "#this.mightExist(#id)")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 前置判断,过滤不存在的ID
public boolean mightExist(Long id) {
return userIdFilter.mightContain(id);
}
}
缓存粒度设计最佳实践
根据数据特性选择合适的缓存粒度:
- 字段级缓存:仅缓存高频访问字段
// 只缓存用户名,适用于频繁查询但不需要完整用户信息的场景
@Cacheable(value = "userNameCache", key = "#userId")
public String getUserName(Long userId) {
User user = userRepository.findById(userId).orElse(null);
return user != null ? user.getName() : null;
}
- 对象级缓存:缓存完整对象,适用于整体查询
@Cacheable(value = "userCache", key = "#id")
public User getFullUser(Long id) {
return userRepository.findById(id).orElse(null);
}
- 列表缓存:注意控制数量和更新策略
// 根据角色缓存用户列表,需要使用复合键
@Cacheable(value = "userListCache", key = "'role:' + #role")
public List<User> getUsersByRole(String role) {
return userRepository.findByRole(role);
}
// 更新单个用户时,清除包含该用户的所有列表缓存
@CacheEvict(value = "userListCache", allEntries = true)
public void updateUserRole(User user) {
userRepository.save(user);
}
缓存击穿防护对比
不同场景下的缓存击穿防护方案:
| 防护机制 | 实现方式 | 性能影响 | 适用场景 |
|---|---|---|---|
| @Cacheable(sync=true) | JVM 级别的锁 | 单机有效,阻塞当前 JVM 线程 | 单体应用,中低并发 |
| 分布式锁 (Redisson) | Redis 分布式锁 | 集群有效,有网络开销 | 分布式系统,中高并发 |
| 热点数据永不过期 | 定时任务异步刷新 | 无阻塞,额外维护成本 | 高并发,可接受短时不一致 |
分布式锁实现示例(需要引入 Redisson 依赖):
@Service
public class HotProductService {
private final RedissonClient redissonClient;
private final ProductRepository productRepository;
// 注入Redisson客户端(需要添加依赖)
public HotProductService(RedissonClient redissonClient, ProductRepository productRepository) {
this.redissonClient = redissonClient;
this.productRepository = productRepository;
}
public Product getHotProduct(Long productId) {
// 先查缓存
Product product = cacheManager.getCache("hotProductCache")
.get(productId, Product.class);
if (product != null) {
return product;
}
// 缓存未命中,使用分布式锁防止击穿
RLock lock = redissonClient.getLock("product_lock:" + productId);
try {
// 尝试获取锁,最多等待500ms,锁有效期10s
if (lock.tryLock(500, 10000, TimeUnit.MILLISECONDS)) {
try {
// 再次检查缓存,双重检查
product = cacheManager.getCache("hotProductCache")
.get(productId, Product.class);
if (product != null) {
return product;
}
// 缓存确实不存在,查询数据库
product = productRepository.findById(productId).orElse(null);
if (product != null) {
cacheManager.getCache("hotProductCache").put(productId, product);
}
return product;
} finally {
// 确保释放锁
lock.unlock();
}
} else {
// 获取锁超时,返回可能的旧数据或默认值
return cacheManager.getCache("hotProductCache")
.get(productId, Product.class);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取分布式锁被中断");
}
}
}
延迟双删策略详解
解决缓存与数据库一致性问题的高级方案:
@Service
public class ConsistentUserService {
private final UserRepository userRepository;
private final CacheManager cacheManager;
public ConsistentUserService(UserRepository userRepository, CacheManager cacheManager) {
this.userRepository = userRepository;
this.cacheManager = cacheManager;
}
@Transactional
public void updateUserWithDelayDoubleDeletion(User user) {
Long userId = user.getId();
// 第一次删除缓存
cacheManager.getCache("userCache").evict(userId);
// 更新数据库
userRepository.save(user);
// 异步延迟再次删除缓存
CompletableFuture.runAsync(() -> {
try {
// 延迟时间需大于业务读取的最大响应时间
Thread.sleep(500);
cacheManager.getCache("userCache").evict(userId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
为什么需要延迟双删?
想象以下场景:
- 线程 A 删除缓存,准备更新数据库
- 此时线程 B 查询,发现缓存不存在,从数据库读取旧数据
- 线程 B 将旧数据写入缓存
- 线程 A 完成数据库更新
结果:缓存中仍然是旧数据!
延迟双删通过等待所有可能的并发读取完成后,再次删除缓存,确保不会有旧数据被重新写入缓存。延迟时间应大于业务读取的最大响应时间,通常为 500ms~2s。
性能对比测试
基于 JMeter 测试结果(100 并发用户,持续 60 秒,10,000 条数据):
graph LR
A[无缓存] --> B[平均响应: 350ms<br>吞吐量: 285 TPS<br>数据库负载: 高]
C[本地缓存] --> D[平均响应: 0.5ms<br>吞吐量: 1850 TPS<br>数据库负载: 低]
E[Redis缓存] --> F[平均响应: 5ms<br>吞吐量: 1650 TPS<br>数据库负载: 低]
style B fill:#f9a,stroke:#333
style D fill:#afa,stroke:#333
style F fill:#aaf,stroke:#333
在 90%缓存命中率情况下:
- 本地缓存性能提升:响应时间降低 700 倍,吞吐量提升 6.5 倍
- Redis 缓存性能提升:响应时间降低 70 倍,吞吐量提升 5.8 倍
缓存常见问题与解决方案
缓存穿透
问题:查询不存在的数据频繁落到数据库
解决方案:
- 缓存空结果(设置较短过期时间)
- 布隆过滤器快速判断(高效但有误判率)
// 缓存空结果示例
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) {
User user = userRepository.findById(id).orElse(null);
if (user == null) {
// 对于不存在的结果,设置一个空对象,避免频繁查询数据库
cacheManager.getCache("userCache").put(id, NullValue.INSTANCE);
}
return user;
}
缓存雪崩
问题:大量缓存同时过期
解决方案:
- 设置随机过期时间,错开失效时间点
- 多级缓存架构,提供降级处理
- 热点数据永不过期(后台定时刷新)
// 随机过期时间配置
.entryTtl(Duration.ofHours(2)
.plusMinutes(new Random().nextInt(60)))
数据一致性策略
不同业务场景下的缓存一致性策略对比:
| 策略 | 实现方式 | 一致性级别 | 适用场景 |
|---|---|---|---|
| 强一致性 | 更新 DB 前删缓存+事务 | 实时一致,性能较低 | 金融交易、库存管理 |
| 最终一致性 | 延迟双删、异步更新 | 短暂不一致,性能较好 | 商品信息、用户资料 |
| 弱一致性 | 定时刷新、过期自动更新 | 可能较长不一致,性能最佳 | 访问量统计、推荐数据 |
总结
| 注解 | 主要作用 | 关键参数 | 适用场景 |
|---|---|---|---|
| @Cacheable | 缓存方法返回值 | value、key、condition、unless、sync | 查询操作,减少重复计算 |
| @CacheEvict | 清除缓存数据 | value、key、allEntries、beforeInvocation | 更新/删除操作,保证数据一致性 |
| @CachePut | 更新缓存数据 | value、key、condition、unless | 更新操作后刷新缓存 |