Опирается на правила:
R-CACHE-KEY-1…R-CACHE-KEY-4иR-CACHE-KEY-X1…R-CACHE-KEY-X4из Caching Rules → раздел 3. Ключи.
Важно знать
- В Go нет декларативного
@Cacheable— ключ формируется явно в функцииadapters/out/cache/keys.go.- Namespace через префикс:
"customer-profiles:" + customerID. Redis key:customer-profiles:42.- Имена namespace в kebab-case:
customer-profiles,order-summaries,feature-flags. Не camelCase, не snake_case.- Составной ключ — явный join через
:, задокументированный в функции.- Sensitive данные (email, phone, токены) в ключе plain-text — утечка в мониторинг. Хешируй через
crypto/sha256.fmt.Sprintf("%v", args...)как ключ — ломается при изменении количества аргументов.- Указатель или адрес struct в ключе — каждый вызов новый ключ,
hit rate = 0%.- Один общий namespace (
shared-cache:) для разных сущностей — TTL, метрики и evict перемешаются.
Ключ — то, по чему Redis находит значение. В Go он формируется явно, без магии фреймворка. Это хорошо: поведение предсказуемо и лежит в одном месте — файле keys.go. Всё, что нужно, это соблюдать три вещи: правильный namespace, явный детерминированный ключ, защита PII.
Namespace через префикс
R-CACHE-KEY-1: каждый тип данных живёт в своём namespace — отдельной строке-префиксе.
// adapters/out/cache/keys.go
func customerSummaryKey(customerID string) string { return "customer-profiles:" + customerID }
func orderSummaryKey(orderID string) string { return "order-summaries:" + orderID }
func productKey(productID string) string { return "product-catalog:" + productID }
func featureFlagKey(name string) string { return "feature-flags:" + name }
Redis key выглядит так: customer-profiles:42. Разделитель : — Redis-конвенция для вложенных namespaces.
Что это даёт:
- Изолированный evict.
DEL customer-profiles:42не затрагиваетorder-summaries:. - Читаемые метрики.
cache_hits_total{cache="customer-profiles"}— статистика по типу данных отдельно. - Навигация в redis-cli.
SCAN 0 MATCH customer-profiles:*— все ключи одного namespace.
Kebab-case для имён namespace
R-CACHE-KEY-2: имена namespace — kebab-case slug, как URL-пути и event-topics.
customer-profiles ✓
order-summaries ✓
feature-flags ✓
payment-methods ✓
customerProfiles ✗ — camelCase
Customer_Profiles ✗ — mixed
shared ✗ — нет смысла
Соглашение сквозное: URL /customer-profiles/42, Redis key customer-profiles:42, Prometheus label cache="customer-profiles" — всё одно имя.
Явный ключ — одна функция на тип
R-CACHE-KEY-3: для каждого типа данных — отдельная именованная функция. Никакого fmt.Sprintf("%v", args...).
Простой ключ по ID:
func customerSummaryKey(customerID string) string {
return "customer-profiles:" + customerID
}
Использование в Use Case Handler:
// core/customer/query_handler.go
func (h *GetCustomerSummaryHandler) Handle(ctx context.Context, q GetCustomerSummaryQuery) (*CustomerSummary, error) {
cached, err := h.cache.GetSummary(ctx, q.CustomerID)
if err == nil && cached != nil {
return cached, nil
}
summary, err := h.repo.GetSummary(ctx, q.CustomerID)
if err != nil {
return nil, err
}
_ = h.cache.SetSummary(ctx, q.CustomerID, summary, h.cfg.Cache.CustomerSummaryTTL)
return summary, nil
}
Адаптер знает только ключ, хендлер знает только customerID — разделение сохранено.
Составной ключ
R-CACHE-KEY-4: несколько параметров — явный join через :, в отдельной именованной функции.
// adapters/out/cache/keys.go
func ordersByCustomerKey(customerID, status string) string {
return "order-summaries:by-customer:" + customerID + ":" + status
}
func productBySlugKey(categoryID, slug string) string {
return "product-catalog:by-slug:" + categoryID + ":" + slug
}
Redis key: order-summaries:by-customer:42:CONFIRMED.
Когда полей больше четырёх — выносим в именованный builder:
type OrderSearchKey struct {
CustomerID string
Status string
FromDate string
ToDate string
Page int
Size int
}
func (k OrderSearchKey) String() string {
return strings.Join([]string{
"order-search",
k.CustomerID,
k.Status,
k.FromDate,
k.ToDate,
strconv.Itoa(k.Page),
strconv.Itoa(k.Size),
}, ":")
}
Использование:
key := OrderSearchKey{
CustomerID: cmd.CustomerID,
Status: cmd.Status,
FromDate: cmd.From.Format("2006-01-02"),
ToDate: cmd.To.Format("2006-01-02"),
Page: cmd.Page,
Size: cmd.Size,
}.String()
Преимущество перед fmt.Sprintf: порядок полей явный, добавление нового поля не ломает старые ключи молча.
Sensitive данные — sha256
R-CACHE-KEY-X4: Redis-ключи видны в redis-cli MONITOR, slow query log, резервных копиях, сетевом перехвате при non-TLS. Email или токен в plain ключе — утечка.
// adapters/out/cache/keys.go
import (
"crypto/sha256"
"encoding/hex"
)
func customerByEmailKey(email string) string {
h := sha256.Sum256([]byte(email))
return "customer-by-email:" + hex.EncodeToString(h[:])
}
func sessionKey(token string) string {
h := sha256.Sum256([]byte(token))
return "sessions:" + hex.EncodeToString(h[:])
}
Лучше — не использовать sensitive данные как идентификатор вообще. Внутренний customerID (UUID или BIGINT) идеален: уникален, не несёт PII, безопасен в ключе.
Пример с OrderID на доменах Sber — безопасно:
// account-id не PII в контексте Redis (UUID, не номер счёта)
func accountSummaryKey(accountID string) string {
return "account-summaries:" + accountID
}
// номер карты — PII, хешируем
func cardLimitKey(maskedPAN string) string {
h := sha256.Sum256([]byte(maskedPAN))
return "card-limits:" + hex.EncodeToString(h[:])
}
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
fmt.Sprintf("%v", args...) как ключ | R-CACHE-KEY-X1 | явная именованная функция |
Указатель struct fmt.Sprintf("%p", &req) в ключе | R-CACHE-KEY-X2 | поля struct через : |
Один namespace shared-cache: для разных сущностей | R-CACHE-KEY-X3 | per-entity префикс |
| PII/токены plain в ключе | R-CACHE-KEY-X4 | crypto/sha256 + hex.EncodeToString |
| camelCase / snake_case в имени namespace | R-CACHE-KEY-2 | kebab-case slug |
| Ключ собирается inline в хендлере | R-CACHE-KEY-3 | функция в keys.go |
Разделитель _ в составном ключе | R-CACHE-KEY-3 | : (Redis-конвенция) |
| Глобальная переменная с форматом ключа | R-CACHE-KEY-3 | именованная функция xyzKey(id) |
Куда дальше
- TTL — per-cache TTL через
CacheConfig;0в go-redis = infinite. - Invalidation —
DeleteSummaryиспользует те же ключевые функции. - Где кешируем — какие проекции стоит ключевать, какие нет.
- Конфигурация —
CacheConfigсenvconfigи per-cache TTL. - Observability — label
cacheв Prometheus = имя namespace. - Cache stampede —
singleflightиSetNX-lock по тому же ключу. - Паттерны — cache-aside, write-through, refresh-ahead на этих ключах.