Опирается на правила:
R-CACHE-CFG-1…R-CACHE-CFG-5иR-CACHE-CFG-X1…R-CACHE-CFG-X4из Caching Style Guide → раздел 2. Конфигурация.
Важно знать
- RedisCacheManager в проде, не in-memory
ConcurrentMapCacheManager. В multi-instance каждый pod имел бы свой локальный кеш.- JSON-сериализация через
GenericJackson2JsonRedisSerializer. Java native — security risk (deserialization CVE).- Per-cache TTL — каждый именованный кеш имеет explicit duration; не один глобальный default.
@ConfigurationProperties+@Validatedдля cache settings — TTL меняется вapplication.yml, не в коде.- В тестах —
ConcurrentMapCacheManagerили Testcontainers Redis. МокиCache-интерфейса теряют поведение TTL/eviction.@EnableCachingбез CacheManager-бина — Spring создаётNoOpCacheManager,@Cacheableмолча ничего не кеширует.
Redis вместо in-memory в проде
R-CACHE-CFG-1: production cache backend — Redis.
Почему не ConcurrentMapCacheManager (Spring default simple):
- Multi-instance. 10 реплик в K8s = 10 локальных кешей. Тот же
userId=42попадает в разные реплики и читает разные значения. Инкосистенция reads. - Invalidation race. Write на pod-1 evict-ит локальный кеш pod-1, но не трогает pod-2 — там старое значение живёт TTL.
- Persistence. Redis может пережить рестарт (RDB/AOF). In-memory — теряет всё при scale-down.
- Observability. Redis-side метрики, cluster mode, отдельный экспортер для Prometheus.
@Configuration
@EnableCaching
@RequiredArgsConstructor
public class CacheConfig {
private final CacheSettings settings;
@Bean
RedisCacheManager cacheManager(RedisConnectionFactory cf, ObjectMapper mapper) {
var defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer(mapper)))
.entryTtl(Duration.ofMinutes(15))
.disableCachingNullValues();
var perCache = settings.caches().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> defaultConfig.entryTtl(e.getValue().ttl())
));
return RedisCacheManager.builder(cf)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(perCache)
.build();
}
}
JSON сериализация — никогда Java native
R-CACHE-CFG-2: значения в Redis сериализуются JSON через GenericJackson2JsonRedisSerializer(mapper).
Почему не JdkSerializationRedisSerializer:
- Security. Java deserialization — известный класс уязвимости (CVE-2015-7501 family). Если attacker может вписать произвольный blob в Redis (через слабый key, через скомпрометированный сервис),
readObject()может выполнить произвольный код. - Читаемость. JSON в Redis viewable через
redis-cli GET cache:user-profiles::42— видно содержимое для дебага. Binary blob — нет. - Forward-compat. Jackson по умолчанию игнорирует unknown fields. Добавили поле в DTO → старые закешированные значения читаются (без нового поля). Java native —
InvalidClassExceptionпри измененииserialVersionUID.
GenericJackson2JsonRedisSerializer хранит class info в JSON ("@class": "ru.vikulinva.UserProfile"), что позволяет десериализовать в правильный тип без явного TypeReference. Цена — pollination class names в Redis; для UCP это приемлемо.
Per-cache configuration
R-CACHE-CFG-3: каждый кеш — свой TTL.
cache:
caches:
user-profiles:
ttl: 15m
currencies:
ttl: 6h
feature-flags:
ttl: 60s
user-balances:
ttl: 30s
order-summaries:
ttl: 5m
user-profiles и feature-flags физически разные данные с разной частотой изменений и разной критичностью — общий TTL компромисс ни для кого. Подробнее — TTL.
@ConfigurationProperties + @Validated
R-CACHE-CFG-4: TTL живёт в application.yml, не в коде.
@ConfigurationProperties("cache")
@Validated
public record CacheSettings(
@Valid @NotEmpty Map<String, CacheConfig> caches
) {
public record CacheConfig(
@NotNull Duration ttl,
boolean disableNullValues
) {}
}
@Configuration
@EnableConfigurationProperties(CacheSettings.class)
public class CacheConfigRegistration {}
Что это даёт:
- SRE/admin меняет TTL в config-map без redeploy. Достаточно перезагрузить config (Spring Cloud Config) или передеплоить с новым ENV.
@Validatedловит invalid YAML при старте (TTLбез значения, отрицательная Duration).@Validв Map нужен Spring 3.0+, иначе nested@NotNullне проверяется.
В тестах — ConcurrentMapCacheManager или Testcontainers
R-CACHE-CFG-5: два режима тестирования кеша.
@TestConfiguration
public class TestCacheConfig {
@Bean
@Primary
CacheManager testCacheManager() {
return new ConcurrentMapCacheManager(
"user-profiles", "currencies", "feature-flags"
);
}
}
ConcurrentMapCacheManager для unit-тестов handler-ов: проверяем «второй вызов handler-а с теми же params не дёрнул repository» через verify(repository, times(1))....
Для integration-тестов, проверяющих real Redis behaviour (TTL, eviction под memory pressure) — Testcontainers:
@SpringBootTest
@Testcontainers
class CacheIntegrationTest {
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@DynamicPropertySource
static void redisProps(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", redis::getFirstMappedPort);
}
}
Не моки Cache-интерфейса через Mockito. Mock возвращает что напишут, теряет реальное поведение TTL и eviction.
Что запрещено
JdkSerializationRedisSerializer
R-CACHE-CFG-X1: security risk + не читается через redis-cli. Всегда GenericJackson2JsonRedisSerializer.
ConcurrentMapCacheManager в multi-instance проде
R-CACHE-CFG-X2: каждый pod = свой кеш, нет consistency. Stage-окружение с одной репликой может работать, прод с 10 репликами — race-условия на каждом write.
Один глобальный TTL
R-CACHE-CFG-X3: одно значение на все кеши = либо user-balances слишком долго stale (15 мин), либо currencies бессмысленно пересчитываются (60 сек). Per-cache config обязателен.
@EnableCaching без CacheManager-бина
R-CACHE-CFG-X4: Spring создаёт NoOpCacheManager по умолчанию.
// КАТАСТРОФА — silent skip
@Configuration
@EnableCaching
public class CacheConfig {
// нет @Bean RedisCacheManager
}
@Cacheable методы выполняются как будто аннотации нет, но без warning. Hit rate всегда 0%, никто не замечает, пока не открывают Grafana и не видят, что кеша не существует.
Всегда явно объявлять @Bean RedisCacheManager или подключать spring-boot-starter-data-redis с правильной configuration.
Что запрещено — таблица
| Антипаттерн | Правило | Что взамен |
|---|---|---|
JdkSerializationRedisSerializer | R-CACHE-CFG-X1 | GenericJackson2JsonRedisSerializer |
ConcurrentMapCacheManager в multi-instance проде | R-CACHE-CFG-X2 | RedisCacheManager |
| Один глобальный TTL | R-CACHE-CFG-X3 | per-cache configuration |
@EnableCaching без CacheManager-бина | R-CACHE-CFG-X4 | явный @Bean RedisCacheManager |
| TTL hard-coded в Java | R-CACHE-CFG-4 | @ConfigurationProperties + application.yml |
Mock Cache через Mockito | R-CACHE-CFG-5 | ConcurrentMapCacheManager или Testcontainers |
disableCachingNullValues() пропущен | R-CACHE-CFG-1 | включить, иначе кеш заполняется null'ами |
Куда дальше
- Caching → раздел 2. Конфигурация — нормативные формулировки.
- Где кешируем — критерии выбора кандидатов.
- Ключи — namespace, SpEL, custom KeyGenerator.
- TTL — типовые значения по характеру данных.
- Observability — hit rate, eviction metrics.
- Validation → @ConfigurationProperties —
R-VLD-CFG-*. - Security style guide — почему JdkSerializationRedisSerializer запрещён.