Опирается на правила:
R-CACHE-KEY-1…R-CACHE-KEY-4иR-CACHE-KEY-X1…R-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-arg | R-CACHE-KEY-X1 | key = "..." SpEL явно |
Object.toString() дефолтный в ключе | R-CACHE-KEY-X2 | SpEL с явными полями |
Один общий cache shared-cache | R-CACHE-KEY-X3 | per-entity |
| PII/токены plain в ключе | R-CACHE-KEY-X4 | hash или internal ID |
| camelCase / snake_case имя кеша | R-CACHE-KEY-2 | kebab-case slug |
key = "#query" (целый объект) | R-CACHE-KEY-X2 | key = "#query.field()" SpEL |
keyGenerator без @Component | R-CACHE-KEY-4 | @Component("xKeyGenerator") bean |
Разделитель _ в composite key | R-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.