Redis与SpringCache整合
GC 日志看起来乱,关键是找准几个核心指标。很多开发者面对 GC 日志不知道该关注什么。本文从实际调优经验出发,讲需要关注什么、忽略什么,帮你快速定位问题。
一、Spring Cache基础
#
1.1 核心注解
| 注解 |
作用 |
| @Cacheable |
先查缓存,没有则执行方法并缓存结果 |
| @CachePut |
执行方法,并将结果放入缓存 |
| @CacheEvict |
从缓存中移除数据 |
| @Caching |
组合多个缓存操作 |
| @CacheConfig |
在类级别统一配置缓存 |
#
1.2 基本使用
@Service public class UserService { @Autowired private UserMapper userMapper; @Cacheable(value = "user", key = "#id") public User getUser(Long id) { return userMapper.findById(id); } @CachePut(value = "user", key = "#user.id") public User updateUser(User user) { userMapper.update(user); return user; } @CacheEvict(value = "user", key = "#id") public void deleteUser(Long id) { userMapper.delete(id); } @CacheEvict(value = "user", allEntries = true) public void clearUserCache() { } }
|
二、整合Redis Cache
#
2.1 添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
|
#
2.2 启用缓存
@SpringBootApplication @EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
|
#
2.3 配置Redis
spring: redis: host: localhost port: 6379 password: database: 0 lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5
|
#
2.4 配置CacheManager
@Configuration public class CacheConfig { @Autowired private RedisConnectionFactory connectionFactory; @Bean public CacheManager cacheManager() { RedisSerializer<String> keySerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(); RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer)) .disableCachingNullValues(); Map<String, RedisCacheConfiguration> configMap = new HashMap<>(); configMap.put("user", defaultConfig.entryTtl(Duration.ofHours(1))); configMap.put("product", defaultConfig.entryTtl(Duration.ofMinutes(10))); configMap.put("session", defaultConfig.entryTtl(Duration.ofMinutes(30))); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(configMap) .transactionAware() .build(); } }
|
三、序列化配置
#
3.1 JSON序列化(推荐)
@Bean public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer( new GenericJackson2JsonRedisSerializer() ) ); }
|
#
3.2 自定义ObjectMapper
@Bean public RedisCacheConfiguration cacheConfiguration() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL ); objectMapper.registerModule(new JavaTimeModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper); return RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(serializer) ); }
|
#
3.3 Kryo序列化(高性能)
public class KryoRedisSerializer<T> implements RedisSerializer<T> { private final Kryo kryo = new Kryo(); public KryoRedisSerializer() { kryo.setRegistrationRequired(false); } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) return new byte[0]; Output output = new Output(1024, -1); kryo.writeClassAndObject(output, t); return output.toBytes(); } @Override @SuppressWarnings("unchecked") public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) return null; Input input = new Input(bytes); return (T) kryo.readClassAndObject(input); } }
|
四、高级使用
#
4.1 条件缓存
@Service public class ProductService { @Cacheable(value = "product", key = "#id", condition = "#result != null and #result.price > 100") public Product getProduct(Long id) { return productMapper.findById(id); } @CachePut(value = "product", key = "#product.id", unless = "#product.price == 0") public Product updateProduct(Product product) { productMapper.update(product); return product; } }
|
#
4.2 SpEL表达式
@Service public class OrderService { @Cacheable(value = "order", key = "#userId + ':' + #status") public List<Order> getOrders(Long userId, String status) { return orderMapper.findByUserIdAndStatus(userId, status); } @Cacheable(value = "order", key = "#query.userId + ':' + #query.status") public List<Order> searchOrders(OrderQuery query) { return orderMapper.search(query); } @Cacheable(value = "order", key = "#root.methodName + ':' + #id") public Order getOrder(Long id) { return orderMapper.findById(id); } }
|
#
4.3 自定义Key生成器
@Component public class CustomKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getSimpleName()).append(":"); sb.append(method.getName()).append(":"); for (Object param : params) { if (param != null) { sb.append(param.toString()).append(":"); } } return sb.toString(); } }
@Cacheable(value = "user", keyGenerator = "customKeyGenerator") public User getUser(Long id) { return userMapper.findById(id); }
|
五、多级缓存
#
5.1 Caffeine + Redis
@Configuration public class MultiLevelCacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory) .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10))) .build(); CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); caffeineCacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(Duration.ofMinutes(1))); return new CompositeCacheManager(caffeineCacheManager, redisCacheManager); } }
|
#
5.2 自定义多级缓存
@Component public class MultiLevelCacheService { @Autowired private CacheManager cacheManager; public <T> T get(String cacheName, Object key, Class<T> type, Supplier<T> loader) { Cache caffeineCache = cacheManager.getCache("caffeine-" + cacheName); Cache.ValueWrapper l1Value = caffeineCache.get(key); if (l1Value != null) { return (T) l1Value.get(); } Cache redisCache = cacheManager.getCache("redis-" + cacheName); Cache.ValueWrapper l2Value = redisCache.get(key); if (l2Value != null) { T value = (T) l2Value.get(); caffeineCache.put(key, value); return value; } T value = loader.get(); if (value != null) { redisCache.put(key, value); caffeineCache.put(key, value); } return value; } }
|
六、缓存失效策略
#
6.1 基于注解的失效
@Service public class UserService { @Cacheable(value = "user", key = "#id") public User getUser(Long id) { return userMapper.findById(id); } @Caching( put = @CachePut(value = "user", key = "#user.id"), evict = { @CacheEvict(value = "user:list", allEntries = true), @CacheEvict(value = "user:stats", key = "#user.id") } ) public User updateUser(User user) { userMapper.update(user); return user; } }
|
#
6.2 定时刷新
@Component public class CacheRefreshScheduler { @Autowired private UserService userService; @Autowired private CacheManager cacheManager; @Scheduled(fixedRate = 300000) public void refreshHotUsers() { List<Long> hotUserIds = Arrays.asList(1L, 2L, 3L, 4L, 5L); for (Long id : hotUserIds) { Cache cache = cacheManager.getCache("user"); User user = userService.getUserFromDB(id); if (user != null) { cache.put(id, user); } } } }
|
七、常见问题
#
7.1 缓存穿透
@Cacheable(value = "user", key = "#id", unless = "#result == null") public User getUser(Long id) { return userMapper.findById(id); }
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .disableCachingNullValues();
|
#
7.2 缓存雪崩
@Bean public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30 + ThreadLocalRandom.current().nextInt(10))); }
|
#
7.3 缓存击穿
@Service public class UserService { @Autowired private RedissonClient redisson; @Cacheable(value = "user", key = "#id") public User getUser(Long id) { RLock lock = redisson.getLock("lock:user:" + id); try { lock.lock(10, TimeUnit.SECONDS); return userMapper.findById(id); } finally { lock.unlock(); } } }
|
八、监控
#
8.1 缓存命中率监控
@Component public class CacheMetrics { @Autowired private CacheManager cacheManager; @Autowired private MeterRegistry meterRegistry; @PostConstruct public void init() { for (String cacheName : cacheManager.getCacheNames()) { Cache cache = cacheManager.getCache(cacheName); if (cache instanceof CaffeineCache) { com.github.benmanes.caffeine.cache.Cache nativeCache = ((CaffeineCache) cache).getNativeCache(); meterRegistry.gauge("cache.size", Tags.of("name", cacheName), nativeCache, c -> c.estimatedSize()); meterRegistry.gauge("cache.hit.rate", Tags.of("name", cacheName), nativeCache, c -> c.stats().hitRate()); } } } }
|
九、总结
| 特性 |
配置方式 |
| 过期时间 |
entryTtl |
| 序列化 |
serializeKeysWith/serializeValuesWith |
| Null缓存 |
disableCachingNullValues |
| Key前缀 |
prefixCacheNameWith |
| 事务 |
transactionAware |
Spring Cache + Redis的核心价值:
- 声明式缓存:通过注解简化缓存操作
- 统一抽象:切换缓存实现不影响业务代码
- 灵活配置:支持过期时间、序列化等自定义
- 多级缓存:可结合本地缓存提升性能
使用建议:
- 合理设计缓存key,避免冲突
- 设置合适的过期时间
- 注意缓存一致性,及时失效
- 监控缓存命中率
- 复杂场景考虑自定义缓存实现
核心要点
关注 Minor GC 和 Full GC 的频率和耗时
年轻代晋升到老年代的对象大小和频率
GC 前后的内存使用变化
使用 jstat、jmap、jvisualvm 等工具辅助分析
总结
GC 调优是一个持续的过程,没有一劳永逸的方案。需要结合业务特点、数据量、响应时间要求来调整。理解 GC 日志是调优的第一步。