Кеширование

Контракт кеширования UCP (R-CACHE-*): где кешируем, ключи и TTL, invalidation, stampede. Java-биндинг (Spring Cache + Redis) — статьи, Python — скиллы ucp-py-caching-*.

Профиль Python: статьи ниже описывают Java-биндинг этого контракта. Python-биндинг (style-guide и скиллы ucp-py-*) — в репозитории скиллов ↗.

Статья внедрена в скилл AI-агента ucp-caching-review / ucp-caching-design / ucp-py-caching-review / ucp-py-caching-design

Контракт этого раздела язык-нейтрален: правила означают одно и то же на любом стеке, меняется только реализация. Биндинги: Java/Spring — статьи этого раздела; Python/FastAPI — скиллы ucp-py-caching-* в репозитории скиллов; Go и Node — в работе.

Свод правил кеширования в Java/Spring-сервисах команды UCP: что кешируем (что НЕ кешируем — чаще), Spring Cache + Redis конфигурация, key/TTL/invalidation, cache-aside vs write-through, защита от cache stampede, observability. Каждое правило идентифицируется кодом (R-CACHE-WHERE-1, R-CACHE-INV-X1) — скилл ucp-caching-review цитирует эти коды в findings.

Гайд опирается на Spring Cache abstraction с Redis backend через spring-boot-starter-data-redis. ConcurrentMapCacheManager (in-memory) — допустим только в тестах. Не покрывает: кеш на стороне клиента (HTTP Cache-Control — это REST API style guide), database-level query plan cache (это PG-side), CDN.

Связанные стандарты:

  • R-RES-FB-1 — fallback может отдавать cached read при отказе внешней системы.
  • AUTH-16 — PII в кеше требует отдельной политики (TTL, encryption at rest).
  • R-JOOQ-MS-3 — кеш не отменяет multiset для eager-fetch, это разные слои защиты.
  • R-VLD-CFG-*@ConfigurationProperties для cache-настроек обязательно @Validated.

Содержание

  1. Где кешируем — R-CACHE-WHERE-*
  2. Конфигурация — R-CACHE-CFG-*
  3. Ключи — R-CACHE-KEY-*
  4. TTL — R-CACHE-TTL-*
  5. Invalidation — R-CACHE-INV-*
  6. Паттерны — R-CACHE-PATTERN-*
  7. Cache stampede — R-CACHE-STAMP-*
  8. Observability — R-CACHE-OBS-*
  9. Антипаттерны — сводка R-CACHE-*-X*

1. Где кешируем

Подробно для человека: Где кешируем — read-heavy данные, money с осторожностью, агрегаты никогда.

Кеширование — оптимизация. Без неё сервис должен работать корректно. Если кеш «обязателен для корректности» — это бизнес-данные, не кеш.

1.1 Обязательно

R-CACHE-WHERE-1 — Кешируй read-heavy + редко меняющиеся данные:

  • Справочники (страны, валюты, тарифы) — TTL минут/часы.
  • User profile / settings — TTL 5–15 минут.
  • Конфигурация feature-flags — TTL 30–60 секунд.
  • Результаты тяжёлых вычислений (агрегации, отчёты) — TTL по бизнес-смыслу.

R-CACHE-WHERE-2 — Кеш денежных данных (баланс, лимиты, available credit) допустим, но только с явной invalidation strategy: TTL коротким (5–30 секунд), @CacheEvict на каждом write-методе того же ресурса, для критичных операций — cache.evict() перед чтением.

R-CACHE-WHERE-3 — Cache-aside (lazy load) — дефолтный паттерн для большинства случаев. Read проходит через @Cacheable, write делает @CacheEvict или явный cache.evict().

1.2 Запрещено

R-CACHE-WHERE-X1 — Кеш на write-path. @Cacheable на createOrder(), confirmPayment() — нонсенс, write-операции не возвращают «то же значение для тех же входов».

R-CACHE-WHERE-X2 — Кеширование доменного агрегата целиком. @Cacheable на OrderRepository.findById(id)Order aggregate с List<OrderItem> + Payment + Shipment. Нарушает границы агрегата (агрегат сам управляет своей целостностью), агрегат может содержать sensitive-данные, и invalidation становится сложной (любой child-update должен evict-ить parent). Кешируй read-проекции (OrderSummary), не агрегаты.

R-CACHE-WHERE-X3 — Money-данные без TTL и invalidation. Кешировать баланс пользователя на 1 час = «потратил, но баланс показывает старый» — рекламация и инцидент.

R-CACHE-WHERE-X4 — Кеш бизнес-критичных данных только для performance-оптимизации, без явной trade-off оценки. Stale-data для money / orders = inconsistent UX.

R-CACHE-WHERE-X5 — Кеш результата валидации / авторизации. JWT-валидация имеет встроенный JWK кеш (5 минут, см. AUTH-5); ABAC-проверки делаются каждый раз — кеш авторизации = security risk при изменении roles.


2. Конфигурация

Подробно для человека: Конфигурация Spring Cache — RedisCacheManager и JSON-сериализация.

2.1 Обязательно

R-CACHE-CFG-1 — В прод-сервисах cache backend — Redis, не in-memory ConcurrentMapCacheManager. Причины:

  • Multi-instance deployment — каждый pod имел бы свой локальный кеш, плюс свои eviction-стратегии → inconsistent reads, лишние invalidation race-conditions.
  • Persistence (опционально) — Redis может пережить рестарт.
  • Observability — Redis-side metrics + cluster mode.

Spring-конфиг:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    RedisCacheManager cacheManager(RedisConnectionFactory cf, ObjectMapper mapper) {
        var config = RedisCacheConfiguration.defaultCacheConfig()
            .serializeValuesWith(SerializationPair.fromSerializer(
                new GenericJackson2JsonRedisSerializer(mapper)))
            .entryTtl(Duration.ofMinutes(15))      // дефолт; per-cache переопределяется
            .disableCachingNullValues();

        var perCache = Map.of(
            "user-profiles", config.entryTtl(Duration.ofMinutes(15)),
            "currencies", config.entryTtl(Duration.ofHours(6)),
            "feature-flags", config.entryTtl(Duration.ofSeconds(60))
        );

        return RedisCacheManager.builder(cf)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(perCache)
            .build();
    }
}

R-CACHE-CFG-2 — Сериализация значений — JSON (GenericJackson2JsonRedisSerializer), никогда Java native (JdkSerializationRedisSerializer). Причины:

  • Security: Java deserialization = известный класс vulnerability (CVE-2015-7501 family).
  • Читаемость: JSON в Redis viewable через redis-cli get; binary blob — нет.
  • Forward-compat: добавление поля в DTO не ломает старый кеш сразу (Jackson игнорирует unknown).

R-CACHE-CFG-3 — Per-cache configuration — каждый именованный кеш имеет explicit TTL. Не полагайся на default-конфиг для всех кешей; разные данные требуют разного TTL.

R-CACHE-CFG-4 — Cache settings — через @ConfigurationProperties (см. R-VLD-CFG-1):

@ConfigurationProperties("cache")
@Validated
public record CacheSettings(
    @Valid @NotEmpty Map<String, CacheConfig> caches
) {
    public record CacheConfig(
        @NotNull Duration ttl,
        boolean disableNullValues
    ) {}
}

В application.yml:

cache:
  caches:
    user-profiles:
      ttl: 15m
      disable-null-values: true
    currencies:
      ttl: 6h

R-CACHE-CFG-5 — В тестах — ConcurrentMapCacheManager либо Testcontainers Redis. Не моки Cache-интерфейса (теряется поведение TTL/eviction).

2.2 Запрещено

R-CACHE-CFG-X1 — JdkSerializationRedisSerializer — security risk, см. R-CACHE-CFG-2.

R-CACHE-CFG-X2 — ConcurrentMapCacheManager (Spring default simple) в multi-instance проде. Каждый pod = свой кеш, нет консистентности.

R-CACHE-CFG-X3 — Один глобальный TTL для всех кешей. Профиль (15 мин) и валюты (6 часов) и feature-flags (60 сек) не должны делить настройку.

R-CACHE-CFG-X4 — @EnableCaching без CacheManager-бина. Spring создаст NoOpCacheManager, @Cacheable молча ничего не кеширует — silent skip.


3. Ключи

Подробно для человека: Ключи кеша — namespace, SpEL и custom KeyGenerator.

3.1 Обязательно

R-CACHE-KEY-1 — Namespace через имя кеша: cacheNames = "user-profiles" — Spring сам префиксует (Redis key: user-profiles::42). Префикс отделяет кеши в Redis, упрощает evict-by-namespace.

R-CACHE-KEY-2 — Имена cache (slug-style, kebab-case): user-profiles, payment-methods, feature-flags. Не camelCase, не PascalCase.

R-CACHE-KEY-3 — Ключ внутри cache — через SpEL explicit:

@Cacheable(cacheNames = "user-profiles", key = "#userId")
public UserProfile findProfile(Long userId) { ... }

@Cacheable(cacheNames = "orders-by-customer", key = "#customerId + ':' + #status")
public List<OrderSummary> findByCustomerAndStatus(Long customerId, OrderStatus status) { ... }

Composite-ключи через + ':' + — простой и читаемый формат.

R-CACHE-KEY-4 — Custom KeyGenerator — только для очень сложных ключей (где SpEL нечитаем). Реализация:

@Component("complexKeyGenerator")
public class ComplexKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        // явная логика
    }
}

Использование: @Cacheable(keyGenerator = "complexKeyGenerator").

3.2 Запрещено

R-CACHE-KEY-X1 — Дефолтный generator (без key-параметра) для методов с несколькими параметрами. Spring сериализует все аргументы через SimpleKey — выглядит как [arg1, arg2], легко ломается при изменении сигнатуры. Указывай key = "..." явно.

R-CACHE-KEY-X2 — Object.toString() или подобное в ключе. Если параметр — DTO с Object.toString() дефолтным (UserProfile@1a2b3c) — каждый вызов = новый ключ.

R-CACHE-KEY-X3 — Один общий cache для разных entity (cacheNames = "shared-cache"). Теряется namespacing, evict пересекается, метрики нечитаемы.

R-CACHE-KEY-X4 — Encoding sensitive данных в ключе (email, phone, токены) plain-text. Redis-keys видны в logs, monitoring tools. Hash значение (Hashing.sha256().hashString(email)) если ключ обязан содержать.


4. TTL

Подробно для человека: TTL — explicit duration per cache и типовые значения по характеру данных.

4.1 Обязательно

R-CACHE-TTL-1 — Каждый cache имеет explicit TTL. Никаких infinite-кешей.

R-CACHE-TTL-2 — Типовые значения по характеру данных:

Тип данныхTTLПример
Static referencehourscurrencies, countries, timezones
User profile / preferences15–30 minUserProfile, UserSettings
Feature flags30–60 secFeatureFlagSet
Configuration overrides60 secTenantConfig
Heavy aggregations5–10 minDailyReport
Money-related5–30 secUserBalance (с явной evict-стратегией)
OAuth/JWT keys5 min (default)JWK Set (см. AUTH-5)

R-CACHE-TTL-3 — TTL — через application.yml, не hard-coded в Java. Это позволяет SRE/admin поднимать/понижать TTL под нагрузку без redeploy.

R-CACHE-TTL-4 — Если cached data имеет естественный invalidation event (например orderConfirmed → invalidate OrderSummary) — TTL longer, invalidation does the heavy lifting. Если invalidation не реализуема — TTL коротким (compromise).

4.2 Запрещено

R-CACHE-TTL-X1 — Cache с TTL = Duration.ZERO или infinite. ZERO — Spring трактует как «no TTL» = forever. Forever в Redis при достижении max-memory приведёт к eviction по LRU/LFU без вашего контроля.

R-CACHE-TTL-X2 — TTL больше 24 часов для бизнес-данных. Сервис рестартует чаще раза в сутки (deploy), и долгий TTL переживает релизы — могут быть закешированные значения с устаревшей структурой DTO.

R-CACHE-TTL-X3 — Money-кеш без TTL или TTL > 1 минута без strict invalidation. См. R-CACHE-WHERE-X3.


5. Invalidation

Подробно для человека: Invalidation — @CacheEvict, @EventListener и distributed-sync.

5.1 Обязательно

R-CACHE-INV-1 — На каждом write-методе того же агрегата — @CacheEvict на затронутые кеши:

@CacheEvict(cacheNames = "user-profiles", key = "#userId")
public void updateProfile(Long userId, ProfileUpdate update) { ... }

R-CACHE-INV-2 — Если write меняет несколько кешей@Caching композит:

@Caching(evict = {
    @CacheEvict(cacheNames = "user-profiles", key = "#userId"),
    @CacheEvict(cacheNames = "user-permissions", key = "#userId")
})
public void updateProfile(Long userId, ProfileUpdate update) { ... }

R-CACHE-INV-3 — При доменных событиях (UserUpdatedEvent) — invalidation через @EventListener:

@EventListener
@CacheEvict(cacheNames = "user-profiles", key = "#event.userId()")
public void onUserUpdated(UserUpdatedEvent event) { /* пустой метод */ }

Это паттерн «invalidation as side-effect of domain event», независимый от того, какой именно use case вызвал изменение.

R-CACHE-INV-4 — Для distributed cache invalidation (Redis pub/sub при изменении на одной из инстансов) — встроенно в Redis backend, отдельной обвязки не нужно.

5.2 Запрещено

R-CACHE-INV-X1 — @CacheEvict(allEntries = true) без причины. Сбрасывает весь cache одной операцией — на холодном старте остальные пользователи получают cache miss → нагрузка на БД спайком. Допустимо только при админских операциях (truncate / rebuild).

R-CACHE-INV-X2 — Полагаться только на TTL для consistency. «Подождёт минуту и обновится» — приемлемо для еволюционирующих feature-flags, неприемлемо для money / orders.

R-CACHE-INV-X3 — Eventual consistency без явной декларации. Если кеш может быть stale — это часть контракта endpoint, документируй в OpenAPI (description: 'Возможна задержка до 30 секунд').


6. Паттерны

Подробно для человека: Паттерны кеширования — cache-aside, write-through и refresh-ahead.

6.1 Обязательно

R-CACHE-PATTERN-1 — Cache-aside (lazy load + write evict) — дефолтный паттерн.

  • Read: @Cacheable — first miss идёт в БД, кладёт в кеш; следующие reads — из кеша.
  • Write: @CacheEvict сбрасывает значение; следующий read load-нет свежее.
  • Преимущества: простой, читаем; кеш отстаёт от БД на TTL (acceptable для большинства).

R-CACHE-PATTERN-2 — Cache-through (write-through) — для высокочастотных read + write одного значения. На write кеш обновляется явно (cache.put(...)), не evict-ится:

@CachePut(cacheNames = "user-profiles", key = "#userId")
public UserProfile updateProfile(Long userId, ProfileUpdate update) {
    // вернёт обновлённый, попадёт в кеш
}

Используй когда: данные сразу нужны после write (тот же flow продолжает работать с ними).

R-CACHE-PATTERN-3 — Refresh-ahead — для критичных hot-данных (главная страница, top-100 продуктов). Реализуется через @Scheduled job, который перезаливает cache до истечения TTL:

@Scheduled(fixedDelay = 30_000)
public void refreshTopProducts() {
    var top = productRepository.findTop100();
    cacheManager.getCache("top-products").put("global", top);
}

Гарантирует «no cache miss for hot keys».

6.2 Запрещено

R-CACHE-PATTERN-X1 — Write-behind (write в кеш, async в БД позже) для money/critical-данных. Crash сервиса до flush в БД = потеря данных.

R-CACHE-PATTERN-X2 — Mix паттернов на одном cache. Если cache user-profiles использует cache-aside в одном методе и write-through в другом — invalidation logic становится непонятной. Один cache = один паттерн.


7. Cache stampede

Подробно для человека: Cache stampede — distributed lock и refresh-ahead против thundering herd.

Cache stampede (thundering herd) — multiple parallel requests на одну и ту же холодную ключ. Все думают «cache miss → fetch from DB», DB получает N одинаковых запросов. Под нагрузкой = инцидент.

7.1 Обязательно

R-CACHE-STAMP-1 — Для локального кеша (ConcurrentMapCacheManager) — sync = true:

@Cacheable(cacheNames = "currencies", sync = true)
public List<Currency> findAll() { ... }

Spring блокирует все параллельные вызовы на одном ключе до завершения первого.

R-CACHE-STAMP-2 — Для distributed cache (Redis) — sync = true не помогает (lock только within JVM). Опции:

  • Distributed lock через Redis SET NX EX или Redisson RLock:
    var lock = redisson.getLock("user-profiles:" + userId + ":lock");
    if (lock.tryLock(100, 30, TimeUnit.MILLISECONDS)) {
        try {
            // double-check cache
            // load from DB, put to cache
        } finally {
            lock.unlock();
        }
    }
    
  • Probabilistic refresh — обновляй cache до истечения TTL с probability возрастающей по мере приближения к expiry. Уменьшает синхронные cache miss.

R-CACHE-STAMP-3 — Для hot keys (top-products) — Refresh-ahead (см. R-CACHE-PATTERN-3) — фоновое обновление cache до expiry. Stampede исключён по дизайну.

7.2 Запрещено

R-CACHE-STAMP-X1 — Игнорировать stampede для hot endpoints. Под рассчитываемой нагрузкой (>100 RPS на endpoint) cache miss всех вместе = DB latency-инцидент.

R-CACHE-STAMP-X2 — Использовать synchronized-блок в Java для distributed-cache защиты. JVM-lock не виден другим инстансам.


8. Observability

Подробно для человека: Observability кеша — hit rate, eviction metrics и Redis-side мониторинг.

8.1 Обязательно

R-CACHE-OBS-1 — Spring Cache + Redis автоматически экспортирует через Micrometer (spring-boot-starter-actuator):

  • cache_gets_total{cache,result=hit|miss}
  • cache_puts_total{cache}
  • cache_evictions_total{cache}
  • cache_size{cache} (для bounded caches)

R-CACHE-OBS-2 — Cache hit rate — основная метрика здоровья кеша. Расчёт: hits / (hits + misses). Алерт при hit rate < 70% для долго существующих кешей — означает либо неподходящий TTL, либо слишком частые invalidation.

R-CACHE-OBS-3 — Логировать eviction (@CacheEvict) на уровне DEBUG с key. Не INFO/WARN — будет шумно.

R-CACHE-OBS-4 — Redis-side метрики: redis_cluster_state, redis_memory_used_bytes, redis_keys_total{db} — мониторятся отдельно (Redis Exporter для Prometheus).

8.2 Запрещено

R-CACHE-OBS-X1 — Отключение Spring Cache metrics (management.metrics.enable.cache=false). Без них SRE не увидит «у нас hit rate 5%, кеш бесполезен».


9. Антипаттерны

АнтипаттернПравилоКорректно
Кеш на write-методеR-CACHE-WHERE-X1только read, write — @CacheEvict
Кеш доменного агрегата целикомR-CACHE-WHERE-X2read-проекции (OrderSummary)
Money-кеш без TTL/invalidationR-CACHE-WHERE-X3, R-CACHE-TTL-X3TTL ≤ 30s + явный evict
Кеш JWT/ABAC-валидацииR-CACHE-WHERE-X5JWK уже встроен (AUTH-5); ABAC — каждый раз
JdkSerializationRedisSerializerR-CACHE-CFG-X1GenericJackson2JsonRedisSerializer
ConcurrentMapCacheManager в multi-instance продеR-CACHE-CFG-X2RedisCacheManager
Один TTL на все кешиR-CACHE-CFG-X3per-cache config
@EnableCaching без CacheManager-бинаR-CACHE-CFG-X4явно сконфигурированный bean
Дефолтный keyGenerator на multi-arg методеR-CACHE-KEY-X1key = "..." SpEL явно
Object.toString() в ключеR-CACHE-KEY-X2bean ID или primitive
Один общий cache shared-cacheR-CACHE-KEY-X3per-entity
PII / токены plain в ключеR-CACHE-KEY-X4hash или ID-replacement
TTL = 0 / infiniteR-CACHE-TTL-X1explicit duration
TTL > 24h для бизнес-данныхR-CACHE-TTL-X2разумные интервалы
@CacheEvict(allEntries=true) без причиныR-CACHE-INV-X1по конкретному ключу
Полагаться только на TTL для money-consistencyR-CACHE-INV-X2явный @CacheEvict на write
Eventual consistency без декларацииR-CACHE-INV-X3в OpenAPI description
Write-behind для moneyR-CACHE-PATTERN-X1write-through или cache-aside
Mix паттернов на одном cacheR-CACHE-PATTERN-X2один cache = один паттерн
Игнорировать stampede на hot endpointsR-CACHE-STAMP-X1distributed lock или refresh-ahead
synchronized для distributed-cache защитыR-CACHE-STAMP-X2Redis lock
Отключение cache metricsR-CACHE-OBS-X1оставить включёнными

Финальная сводка: правил «Обязательно» — около 20, «Запрещено» — около 18.