Redis与SpringCache整合

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(); // 不缓存null

// 针对不同cacheName的配置
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 {

// 只有价格大于100才缓存
@Cacheable(value = "product", key = "#id", condition = "#result != null and #result.price > 100")
public Product getProduct(Long id) {
return productMapper.findById(id);
}

// 除非价格为0,否则更新缓存
@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 {

// 使用多个参数作为key
@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) {
// Redis CacheManager
RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)))
.build();

// Caffeine CacheManager
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(1)));

// 组合CacheManager
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) {
// L1: Caffeine
Cache caffeineCache = cacheManager.getCache("caffeine-" + cacheName);
Cache.ValueWrapper l1Value = caffeineCache.get(key);
if (l1Value != null) {
return (T) l1Value.get();
}

// L2: Redis
Cache redisCache = cacheManager.getCache("redis-" + cacheName);
Cache.ValueWrapper l2Value = redisCache.get(key);
if (l2Value != null) {
T value = (T) l2Value.get();
// 回填L1
caffeineCache.put(key, value);
return value;
}

// L3: 数据库
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) // 每5分钟
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 缓存穿透

// 使用unless避免缓存null
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUser(Long id) {
return userMapper.findById(id);
}

// 或使用自定义CacheManager配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues(); // 不缓存null

#

7.2 缓存雪崩

// 使用随机过期时间
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30 + ThreadLocalRandom.current().nextInt(10)));
}

#

7.3 缓存击穿

// Spring Cache本身不提供互斥锁
// 需要结合分布式锁
@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的核心价值:

  1. 声明式缓存:通过注解简化缓存操作
  2. 统一抽象:切换缓存实现不影响业务代码
  3. 灵活配置:支持过期时间、序列化等自定义
  4. 多级缓存:可结合本地缓存提升性能

使用建议:

  1. 合理设计缓存key,避免冲突
  2. 设置合适的过期时间
  3. 注意缓存一致性,及时失效
  4. 监控缓存命中率
  5. 复杂场景考虑自定义缓存实现

核心要点

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

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

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

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

总结

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


   转载规则


《Redis与SpringCache整合》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录