Какие данные кэшируются и как работает кэширование
Что проверяют
Понимаете ли вы, какие данные можно отдавать из кэша без вреда, и умеете ли реализовать кэш в Go так, чтобы он не стал источником гонок, утечек памяти и устаревших ответов. Следом обычно спрашивают про инвалидацию и про ситуацию, когда тысяча запросов одновременно промахнулась мимо кэша.
Как отвечать
Кэшируют то, что часто читают, редко меняют и дорого получают: справочники, конфигурацию, профили, результаты тяжёлых запросов и ответы внешних API. Не кэшируют данные, где устаревшее значение опасно: балансы, остатки при оформлении заказа, права доступа без короткого TTL.
Уровни кэша в Go-сервисе:
- в памяти процесса:
mapподsync.RWMutex,sync.Mapдля сценариев «записали раз, читаем много», библиотеки с вытеснением вродеhashicorp/golang-lru/v2иristretto. Для миллионов записей берутbigcacheилиfreecache: они хранят[]byteбез указателей, и сборщик мусора не сканирует гигантскую карту; - распределённый: Redis через
github.com/redis/go-redis/v9или Memcached, общий для всех реплик; - HTTP:
Cache-ControlиETag, кэш на стороне клиента или CDN.
Стратегии: cache-aside (прочитали из кэша, при промахе сходили в базу и положили), write-through (пишем в базу и кэш синхронно), write-behind (сначала кэш, база позже, риск потерь). Вытеснение по LRU или LFU плюс TTL.
От «стада» при промахе защищает golang.org/x/sync/singleflight: одновременные запросы по одному ключу превращаются в один поход в базу. К TTL добавляют случайный разброс, чтобы ключи не протухали одновременно.
Где спотыкаются
Используют обычную map из нескольких горутин без мьютекса. Рантайм падает с fatal error: concurrent map writes, и recover это не перехватывает.
Держат кэш в памяти пода и забывают, что реплик несколько: после записи одна отдаёт новое значение, остальные старое.
Обновляют кэш перед записью в базу. Если транзакция откатилась, в кэше остаются данные, которых нет. Надёжнее писать в базу и удалять ключ.