Какие есть варианты кэширования на уровне приложения?
Что проверяют
Понимаете ли вы, где держать кэш в Go-сервисе и чем за это платите: памятью и работой GC, устаревшими данными, согласованностью между репликами. Следом обычно спрашивают, как защититься от одновременного промаха по одному ключу, когда сотня горутин разом идёт в базу.
Как отвечать
Разложите по уровням:
- локальный в процессе:
mapподsync.RWMutexс TTL для простых случаев.sync.Mapрассчитан на особые профили (ключ пишется один раз и много читается) и ничего не вытесняет; - готовые библиотеки:
golang-lru/v2с дженериками,ristrettoс учётом частоты обращений,bigcacheиfreecacheдля миллионов записей: они хранят значения в больших байтовых срезах без указателей, и GC не сканирует каждую запись; - распределённый: Redis через
github.com/redis/go-redis/v9или Memcached. Нужен, когда реплик несколько и данные должны быть общими, заодно кэш переживает рестарт сервиса; - двухуровневый: маленький локальный кэш с коротким TTL перед Redis, снимает сетевой запрос с самых горячих ключей;
- HTTP: заголовки
Cache-Control,ETagи ответ304 Not Modified, кэш на CDN или обратном прокси перед сервисом.
Назовите стратегии: cache-aside (читаем кэш, при промахе идём в базу и кладём результат), write-through, инвалидация по TTL или по событию. Защита от лавины промахов: golang.org/x/sync/singleflight склеивает одинаковые запросы в один, а случайный разброс TTL не даёт ключам истечь одновременно.
Где спотыкаются
Пишут в обычный map из нескольких горутин без мьютекса. Рантайм это замечает и падает с fatal error: concurrent map writes, которую не перехватить через recover.
Держат локальный кэш на нескольких репликах и удивляются, что после обновления пользователи видят разные данные в зависимости от того, куда попал запрос.
Советуют кэш на стороне базы вроде MySQL Query Cache. Его удалили ещё в MySQL 8.0, а в PostgreSQL кэша результатов запросов нет вовсе, только буферный кэш страниц.