Опирается на правила: R-CACHE-KEY-1R-CACHE-KEY-4 и R-CACHE-KEY-X1R-CACHE-KEY-X4 из Caching Style Guide → раздел 3. Ключи.

Важно знать

  • Namespace через cacheNames — Spring сам префиксует Redis key (user-profiles::42).
  • Имена в kebab-case slug: user-profiles, payment-methods, feature-flags. Не camelCase, не PascalCase.
  • Ключ через SpEL explicit: key = "#userId", composite через + ':' +.
  • Custom KeyGenerator — только для очень сложных ключей, где SpEL нечитаем. Реализация в @Component.
  • Дефолтный generator на multi-arg — ломается при изменении сигнатуры. Всегда key = "..." явно.
  • Sensitive данные в ключе (email, phone, токены) — хешировать через SHA-256, не plain.
  • Один общий cache shared-cache для разных entity — теряется namespacing, метрики нечитаемы.

Ключ — то, по чему cache находит значение. От его дизайна зависят hit rate, простота invalidation, читаемость метрик. UCP формулирует правила так, чтобы ключ всегда был predictable, readable, secure.

Namespace через cacheNames

R-CACHE-KEY-1: имя кеша работает как Redis-namespace.

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

В Redis ключ выглядит так: user-profiles::42. Spring сам префиксует значением cacheNames, разделитель ::.

Что это даёт:

  • Логическое разделение. KEYS user-profiles::* — все user profiles.
  • Evict-by-namespace. cacheManager.getCache("user-profiles").clear() сбрасывает только этот cache, не трогая остальные.
  • Метрики per cache. cache_gets_total{cache="user-profiles"} — статистика отдельная.

Kebab-case slug для имени

R-CACHE-KEY-2: имена в kebab-case, не camelCase.

user-profiles            ✓
payment-methods          ✓
feature-flags            ✓
order-summaries          ✓
userProfiles             ✗ — camelCase
UserProfiles             ✗ — PascalCase
user_profiles            ✗ — snake_case

Соглашение: Redis-keys в UCP — slug-формата, как URL paths. Это даёт consistency между кешами, путями API (/user-profiles/42) и event-topics (user-profile.updated).

SpEL key explicit

R-CACHE-KEY-3: ключ всегда через key = "...".

Один параметр:

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

Composite ключ:

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

Получится Redis key orders-by-customer::42:CONFIRMED. Разделитель : — конвенция, унаследованная от Redis (вложенные namespaces).

Объект-параметр:

@Cacheable(
    cacheNames = "search-results",
    key = "#query.text() + ':' + #query.limit()"
)
public List<Product> search(SearchQuery query) { ... }

Никогда key = "#query" — Spring вызывает toString(), по умолчанию это SearchQuery@1a2b3c (адрес объекта), ключи никогда не совпадают, hit rate 0%.

Custom KeyGenerator

R-CACHE-KEY-4: для очень сложных ключей выносим в @Component.

@Component("orderSearchKeyGenerator")
public class OrderSearchKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        if (params.length != 1 || !(params[0] instanceof OrderSearchCriteria criteria)) {
            throw new IllegalArgumentException("Expected OrderSearchCriteria");
        }
        return String.join(":",
            String.valueOf(criteria.customerId()),
            criteria.status().name(),
            criteria.fromDate().toString(),
            criteria.toDate().toString(),
            String.valueOf(criteria.page()),
            String.valueOf(criteria.size())
        );
    }
}

Использование:

@Cacheable(cacheNames = "order-search", keyGenerator = "orderSearchKeyGenerator")
public Page<OrderSummary> search(OrderSearchCriteria criteria) { ... }

Когда применять: 4+ полей в ключе, или нужна нормализация (lowercase, date truncation, sorted lists), или явная валидация типа.

Что запрещено

Дефолтный generator на multi-arg

R-CACHE-KEY-X1: без key = "..." Spring использует SimpleKey(arg1, arg2, ...).

// ОПАСНО — SimpleKey сериализуется как [arg1, arg2]
@Cacheable(cacheNames = "orders-by-customer")
public List<OrderSummary> findByCustomerAndStatus(Long customerId, OrderStatus status) { ... }

Что ломается:

  • Изменили порядок параметров — (status, customerId) → ключ изменился, hit rate упал в 0.
  • Добавили параметр — старый кеш мёртв.
  • SimpleKey toString() формат: SimpleKey[42,CONFIRMED] — нечитаемо в Redis.

Всегда key = "..." явно.

Object.toString() в ключе

R-CACHE-KEY-X2: если параметр — DTO без переопределённого toString(), дефолтный возвращает ClassName@hashcode.

// КАТАСТРОФА — каждый вызов = новый ключ
@Cacheable(cacheNames = "search", key = "#query")
public List<Product> search(SearchQuery query) { ... }

SearchQuery@1a2b3c — это identity hash, для каждого нового объекта другой. hit rate = 0%. Если параметр — record (record SearchQuery(...)), toString() переопределён автоматически, но всё равно используем SpEL key = "#query.text() + ':' + #query.limit()" для контроля.

Один общий cache shared-cache

R-CACHE-KEY-X3: соблазн «всё в одном кеше».

// ПЛОХО
@Cacheable(cacheNames = "shared", key = "'user:' + #userId")
public UserProfile findUser(Long userId) { ... }

@Cacheable(cacheNames = "shared", key = "'order:' + #orderId")
public Order findOrder(Long orderId) { ... }

Проблемы:

  • TTL общий — user-profile и order не могут иметь разный TTL.
  • Метрики смешанныеcache_gets_total{cache="shared"} не отделяет hit/miss по типу данных.
  • Evict-all катастрофиченcacheManager.getCache("shared").clear() сбрасывает всё.

Per-entity кеш:

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

@Cacheable(cacheNames = "orders", key = "#orderId")
public Order findOrder(Long orderId) { ... }

Plain PII/токенов в ключе

R-CACHE-KEY-X4: Redis-keys видны в:

  • redis-cli MONITOR логе (для admin).
  • Slow query log.
  • Backup snapshot.
  • Network capture при non-TLS.

Email или токен в plain ключе — leak.

// ПЛОХО
@Cacheable(cacheNames = "user-by-email", key = "#email")
public UserProfile findByEmail(String email) { ... }

// ХОРОШО — hash
@Cacheable(cacheNames = "user-by-email", key = "T(...HashUtil).sha256(#email)")
public UserProfile findByEmail(String email) { ... }

Лучше — никогда не использовать sensitive значения как идентификаторы. Внутренний userId (BIGINT) идеален.

Что запрещено — таблица

АнтипаттернПравилоЧто взамен
Дефолтный generator на multi-argR-CACHE-KEY-X1key = "..." SpEL явно
Object.toString() дефолтный в ключеR-CACHE-KEY-X2SpEL с явными полями
Один общий cache shared-cacheR-CACHE-KEY-X3per-entity
PII/токены plain в ключеR-CACHE-KEY-X4hash или internal ID
camelCase / snake_case имя кешаR-CACHE-KEY-2kebab-case slug
key = "#query" (целый объект)R-CACHE-KEY-X2key = "#query.field()" SpEL
keyGenerator без @ComponentR-CACHE-KEY-4@Component("xKeyGenerator") bean
Разделитель _ в composite keyR-CACHE-KEY-3: (Redis convention)

Куда дальше

  • Caching → раздел 3. Ключи — нормативные формулировки.
  • Где кешируем — какие данные стоит ключевать.
  • TTL — per-cache TTL.
  • Invalidation — @CacheEvict(key = ...) использует те же SpEL-правила.
  • Конфигурация — per-cache config с TTL.
  • Security style guide — почему sensitive данные в Redis-key — leak.