SpringCache缓存抽象层

SpringCache缓存抽象层

GC 日志看起来乱,关键是找准几个核心指标。很多开发者面对 GC 日志不知道该关注什么。本文从实际调优经验出发,讲需要关注什么、忽略什么,帮你快速定位问题。

启用缓存

@Configuration
@EnableCaching
public class CacheConfig {
}

核心注解

注解 作用
@Cacheable 有缓存则返回,无缓存则执行方法并缓存
@CachePut 执行方法并更新缓存
@CacheEvict 清除缓存
@Caching 组合多个缓存操作
@CacheConfig 类级别共享缓存配置

基本使用

#

@Cacheable

@Service
public class UserService {

@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
// 只会在缓存不存在时执行
return userDao.findById(id);
}

@Cacheable(value = "users", key = "#username")
public User getUserByUsername(String username) {
return userDao.findByUsername(username);
}

// 多参数
@Cacheable(value = "users", key = "#id + '_' + #type")
public User getUser(Long id, String type) {
return userDao.findByIdAndType(id, type);
}

// 使用 root 对象
@Cacheable(value = "users", key = "#root.methodName + '_' + #id")
public User getUserV2(Long id) {
return userDao.findById(id);
}

// 条件缓存
@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public User getUserIfPositive(Long id) {
return userDao.findById(id);
}

// unless:结果不满足条件时缓存
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserUnlessNull(Long id) {
return userDao.findById(id);
}
}

#

@CachePut

@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userDao.save(user);
}

#

@CacheEvict

@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userDao.deleteById(id);
}

// 清空整个缓存
@CacheEvict(value = "users", allEntries = true)
public void clearCache() {
}

// 在方法前清除
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUserBefore(Long id) {
userDao.deleteById(id);
}

#

@Caching

@Caching(
put = {
@CachePut(value = "users", key = "#user.id"),
@CachePut(value = "users", key = "#user.username")
},
evict = {
@CacheEvict(value = "userList", allEntries = true)
}
)
public User saveUser(User user) {
return userDao.save(user);
}

#

@CacheConfig

@CacheConfig(cacheNames = "users")
@Service
public class UserService {

@Cacheable(key = "#id") // 不需要写 value
public User getUser(Long id) {
return userDao.findById(id);
}

@CacheEvict(key = "#id")
public void deleteUser(Long id) {
userDao.deleteById(id);
}
}

缓存配置

#

Caffeine 本地缓存

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return cacheManager;
}
}

#

Redis 缓存

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));

return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}

#

多级缓存

@Configuration
@EnableCaching
public class CacheConfig {

@Primary
@Bean
public CacheManager cacheManager() {
// 本地缓存 Caffeine
CaffeineCacheManager localCache = new CaffeineCacheManager();
localCache.setCaffeine(Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES));

return localCache;
}

@Bean
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
// Redis 缓存
return RedisCacheManager.builder(factory).build();
}
}
@Service
public class UserService {

@Cacheable(value = "users", cacheManager = "cacheManager")
public User getUserFromLocal(Long id) {
return userDao.findById(id);
}

@Cacheable(value = "users:redis", cacheManager = "redisCacheManager")
public User getUserFromRedis(Long id) {
return userDao.findById(id);
}
}

SpEL 表达式

@Cacheable(value = "users", key = "#id")
@Cacheable(value = "users", key = "#user.id")
@Cacheable(value = "users", key = "#root.args[0]")
@Cacheable(value = "users", key = "T(java.util.Objects).hash(#id, #name)")
@Cacheable(value = "users", key = "'user_' + #id")
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")

自定义 KeyGenerator

@Component
public class CustomKeyGenerator implements KeyGenerator {

@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "_"
+ method.getName() + "_"
+ StringUtils.arrayToDelimitedString(params, "_");
}
}

缓存问题及解决

#

缓存穿透

问题:查询不存在的数据,每次都会打到数据库。

解决

@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUser(Long id) {
return userDao.findById(id);
}

// 或使用空值缓存
@Cacheable(value = "users", key = "#id")
public Optional<User> getUserOptional(Long id) {
return Optional.ofNullable(userDao.findById(id));
}

#

缓存击穿

问题:热点 key 过期,大量请求同时打到数据库。

解决

@Cacheable(value = "users", key = "#id")
@DistributedLock(key = "'lock:user:' + #id")
public User getUser(Long id) {
return userDao.findById(id);
}

#

缓存雪崩

问题:大量 key 同时过期。

解决

// 随机过期时间
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10 + (int)(Math.random() * 5), TimeUnit.MINUTES));
return manager;
}

总结

Spring Cache 简化了缓存的使用:

注解 场景
@Cacheable 读缓存
@CachePut 写缓存
@CacheEvict 删缓存
@CacheConfig 类级配置

结合 Caffeine(本地)和 Redis(分布式),可以构建高效的缓存层。

核心要点

  1. 关注 Minor GC 和 Full GC 的频率和耗时

  2. 年轻代晋升到老年代的对象大小和频率

  3. GC 前后的内存使用变化

  4. 使用 jstat、jmap、jvisualvm 等工具辅助分析

总结

GC 调优是一个持续的过程,没有一劳永逸的方案。需要结合业务特点、数据量、响应时间要求来调整。理解 GC 日志是调优的第一步。


   转载规则


《SpringCache缓存抽象层》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录