在Java企业级应用开发中,缓存是提升系统性能的利器。Spring框架从3.1版本开始引入了一套声明式的缓存抽象,通过简单的注解就能实现复杂的缓存逻辑。这套机制的核心价值在于:开发者无需关心底层缓存实现(Redis、Ehcache等),只需通过注解配置就能获得一致的缓存体验。
我曾在电商促销系统中使用Spring缓存注解,将商品详情查询的响应时间从平均200ms降低到50ms以下。这种性能提升的关键就在于合理运用了@Cacheable、@CacheEvict等核心注解。下面我将结合实战经验,详细解析这些注解的每个属性细节。
这是最常用的缓存注解,标记方法的返回值应当被缓存。当方法被调用时,Spring会先检查缓存中是否已存在对应的键值:
java复制@Cacheable(
value = "products",
key = "#productId",
condition = "#productId > 1000",
unless = "#result.price < 500"
)
public Product getProductDetail(long productId) {
// 数据库查询逻辑
}
关键属性解析:
value/cacheNames:指定缓存名称,对应配置中的缓存管理器key:SpEL表达式生成的缓存键,默认使用方法参数condition:执行缓存前的条件判断unless:缓存结果后的否决条件实际踩坑:condition和unless的判断时机不同,condition在方法执行前评估,unless在方法执行后评估。曾因混淆两者导致缓存穿透问题。
与@Cacheable不同,@CachePut始终会执行方法体,并用返回值更新缓存。适用于数据更新场景:
java复制@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
// 更新数据库
return product; // 这个返回值会被缓存
}
特殊场景处理:
当方法返回void时,缓存将存储null值。建议在更新操作中始终返回实体对象。
用于删除缓存条目,支持批量清除:
java复制@CacheEvict(
value = "products",
key = "#productId",
allEntries = false, // 是否清空整个缓存区域
beforeInvocation = false // 调用前/后执行清除
)
public void deleteProduct(long productId) {
// 删除数据库记录
}
性能优化技巧:
对于批量删除操作,设置allEntries=true比遍历删除更高效,但要注意这会影响同缓存区的其他数据。
当需要多个缓存操作时,可以用@Caching组合:
java复制@Caching(
cacheable = {
@Cacheable(value = "products", key = "#id")
},
evict = {
@CacheEvict(value = "recommendations", key = "#id")
}
)
public Product getProductWithRecommendation(long id) {
// 复杂查询逻辑
}
简化类中方法的重复配置:
java复制@CacheConfig(cacheNames = "products")
public class ProductService {
@Cacheable(key = "#id")
public Product getById(long id) {...}
}
当默认的键生成策略不满足需求时,可以实现KeyGenerator接口:
java复制@Component
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName() + "_" + Arrays.toString(params);
}
}
// 使用示例
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
结合本地缓存与分布式缓存:
java复制@Caching(
cacheable = {
@Cacheable(value = "local_products", key = "#id"),
@Cacheable(value = "redis_products", key = "#id")
}
)
public Product getProductWithMultiCache(long id) {...}
对于可能被恶意攻击的查询方法:
java复制@Cacheable(
value = "products",
key = "#id",
condition = "#id != null && #id > 0",
unless = "#result == null"
)
public Product getProductSafely(Long id) {
if(id == null || id <= 0) return null;
// 正常查询逻辑
}
现象: 更新数据后缓存未刷新
排查步骤:
现象: Redis缓存报序列化错误
解决方案:
通过设置不同的过期时间分散风险:
java复制@Cacheable(
value = "products",
key = "#id",
cacheResolver = "randomTtlCacheResolver" // 自定义解析器
)
public Product getProductWithRandomTtl(long id) {...}
在最近的一个高并发项目中,通过调整@Cacheable的sync属性,将缓存击穿导致的数据库压力降低了80%:
java复制@Cacheable(value = "hot_products", key = "#id", sync = true)
public Product getHotProduct(long id) {...}
这个配置使得在高并发场景下,只有一个线程会执行方法体,其他线程等待缓存结果,有效保护了后端数据库。