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); } @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); } @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") 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() { CaffeineCacheManager localCache = new CaffeineCacheManager(); localCache.setCaffeine(Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES)); return localCache; } @Bean public CacheManager redisCacheManager(RedisConnectionFactory factory) { 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(分布式),可以构建高效的缓存层。
核心要点
关注 Minor GC 和 Full GC 的频率和耗时
年轻代晋升到老年代的对象大小和频率
GC 前后的内存使用变化
使用 jstat、jmap、jvisualvm 等工具辅助分析
总结
GC 调优是一个持续的过程,没有一劳永逸的方案。需要结合业务特点、数据量、响应时间要求来调整。理解 GC 日志是调优的第一步。