@Cacheable выглядит просто. До первого падения сериализации в production или гонки при cache stampede.🔹 Решение
▪️ Конфигурация RedisCacheManager
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // null не кешируем
Map<String, RedisCacheConfiguration> configs = Map.of(
"users", defaults.entryTtl(Duration.ofHours(1)),
"products", defaults.entryTtl(Duration.ofMinutes(5)),
"sessions", defaults.entryTtl(Duration.ofDays(1))
);
return RedisCacheManager.builder(factory)
.cacheDefaults(defaults)
.withInitialCacheConfigurations(configs)
.build();
}
}
▪️ Использование с явным ключом
@Cacheable(value = "users", key = "#userId", unless = "#result == null")
public User findById(Long userId) { ... }
@CacheEvict(value = "users", key = "#user.id")
public User update(User user) { ... }
// Evict всего кеша при массовых операциях
@CacheEvict(value = "users", allEntries = true)
public void importUsers(List<User> users) { ... }
▪️ Защита от cache stampede через @CachePut + Lock
@Service
public class ProductService {
private final Map<Long, Object> locks = new ConcurrentHashMap<>();
public Product getProduct(Long id) {
Product cached = cacheManager.getCache("products").get(id, Product.class);
if (cached != null) return cached;
// только один поток пересчитывает, остальные ждут
Object lock = locks.computeIfAbsent(id, k -> new Object());
synchronized (lock) {
// double-check после захвата лока
cached = cacheManager.getCache("products").get(id, Product.class);
if (cached != null) return cached;
Product product = repository.findById(id).orElseThrow();
cacheManager.getCache("products").put(id, product);
return product;
}
}
}
Сериализация через
GenericJackson2JsonRedisSerializer сохраняет информацию о типе в JSON (@class поле). Это позволяет десериализовать полиморфные структуры, но требует чтобы все классы в графе объекта были сериализуемы Jackson'ом. Проблема обычно вылезает при кешировании Hibernate entity с lazy коллекциями — они не сериализуются и роняют приложение.⚠️ Никогда не кешируйте Hibernate entity напрямую. Создавайте DTO, сериализуйте его.
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#Enterprise