Читай реальный код и конфиг кэширования, затем рассуждай о поведении: гонка устаревшего чтения, расчёт от доли промахов к нагрузке БД, misconfiguration noeviction и стампида горячего ключа в пути чтения.
SDSenior◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Баги кэширования живут в коде и конфиге, не в прозе: пропущенная инвалидация, синхронный путь промаха, дефолтная политика вытеснения, чтение горячего ключа без коалесцирования. Читай каждый сниппет, рассуждай, что он делает под нагрузкой, и выбирай ответ, на который подписался бы senior-инженер.
Практикуй цикл, что прогоняешь на ревью дизайна кэширования или инцидента: найди стратегию в коде, предскажи её поведение под конкурентностью и нагрузкой и выбери изменение, что механика реально поддерживает.
Сниппет 1 — порядок инвалидации
# чтение cache-aside + инвалидация «удаляй на записи»def get_user(uid): u = cache.get(f"user:{uid}") if u is None: # промах u = db.read_user(uid) # медленная загрузка ТЕКУЩЕГО значения cache.set(f"user:{uid}", u) # заполнить (без TTL) return udef update_user(uid, patch): db.write_user(uid, patch) # новое значение в БД cache.delete(f"user:{uid}") # инвалидировать
Викторина
Completed
Под конкурентностью устаревшее значение пользователя изредка переживает обновление навсегда. В чём баг и самая надёжная починка?
Heads-up Оно корректно в изоляции, но гоняется под конкурентностью: медленный читатель, держащий старое значение, может записать его после delete, и без TTL оно никогда не самоисправится. Версионированные ключи убирают общий мутируемый ключ; TTL-подстраховка хотя бы ограничивает ущерб.
Heads-up Заполнение при промахе — нормальный cache-aside. Баг в переплёте set медленного чтения с delete записи, плюс нет TTL для восстановления. Чини гонку (версионированные ключи) и добавь TTL-подстраховку, а не шаг заполнения.
Heads-up Больше узлов не чинит логическую гонку на одном ключе — устаревший set-после-delete случается независимо от числа узлов. Используй версионированные ключи, чтобы устаревшая запись легла на осиротевшую старую версию, и добавь TTL, чтобы пропущенная инвалидация самоисцелилась.
Что возвращают before и after и каков вывод для базы?
Heads-up Ты посчитал чтения, отданные из КЭША (hit_ratio × total), а не промахи, доходящие до БД. db_reads = total × (1 − hit_ratio): 800, затем 8 000. БД видит промахи, что подскочили в 10×.
Heads-up Числа верны, но вывод неверен: 800 → 8 000 это рост нагрузки в 10×, не незначительный. Изменение hit ratio выглядит малым именно потому, что опасность живёт на стороне промахов, что умножилась вдесятеро.
Heads-up Поглощаются лишь попадания; промахи проваливаются. При 99% всё равно 800 промахов/с, а при 90% — 8 000, скачок в 10×. Считай total × (1 − hit_ratio), чтобы увидеть, что БД реально чувствует.
Сниппет 3 — конфиг кэша Redis
# инстанс redis, используемый чисто как кэшmaxmemory 16gbmaxmemory-policy noeviction # оставлен дефолт# приложение делает SET ... EX 600 для каждого ключа
Викторина
Completed
Что происходит, когда этот кэш заполняет свои 16ГБ, и какой должна быть политика?
Heads-up noeviction защищает надёжное хранилище отказом от записей под давлением, но кэш расходуем: ты ХОЧЕШЬ, чтобы он сбрасывал холодные ключи и продолжал отдавать. Падающие SET под нагрузкой — это сбой. Используй allkeys-lru/lfu для кэша.
Heads-up noeviction НЕ перезаписывает ничего — он отклоняет новые записи с ошибкой, когда полон. Чтобы освободить место сбросом холодных ключей, надо задать политику вытеснения вроде allkeys-lru/lfu.
Heads-up volatile-lru вытесняет лишь ключи с TTL; любой ключ без TTL невытесняем и тихо забивает память, пока вытеснение не остановится. Раз ты хочешь любой ключ вытесняемым для чистого кэша, используй allkeys-lru/lfu.
Сниппет 4 — путь чтения горячего ключа
# страница товара вирусного айтема, пересчёт ~300мсdef get_page(pid): html = cache.get(f"page:{pid}") if html is None: # промах html = render_page(pid) # дорого: 6 запросов + рендер cache.set(f"page:{pid}", html, ex=30) return html# на пике ~40 000 параллельных запросов на ОДИН pid
Викторина
Completed
Каждые 30 секунд бэкенд бьют тысячи одновременных вызовов render_page на один pid. В чём сбой и починка?
Heads-up Длиннее TTL лишь делает стампиду каждые 300с вместо 30с — она всё равно детонирует при истечении тысячами одновременных рендеров. Коалесцируй промахи (single-flight), чтобы параллельные читатели делили один пересчёт.
Heads-up Быстрее рендер уменьшает каждую стампиду, но не останавливает тысячи одновременных пересчётов одного значения. Структурная починка — коалесцирование параллельных промахов в один пересчёт, как бы быстр ни был render_page.
Heads-up Один ключ нельзя разбить по шардам — шардирование не помогает горячему ключу. Периодический скачок при истечении — это стампида; чини через single-flight (и подумай о near-кэше/репликации горячего ключа для постоянной нагрузки).
Вспомните перед уходом
01
Почему удаление-на-записи гоняется и какова надёжная починка?
02
Как из hit ratio посчитать нагрузку БД и почему асимметрия опасна?
03
Почему noeviction неверен для кэша и как остановить стампиду горячего ключа в пути чтения?
Итог
Каждое решение кэширования в этом разделе всплывает прямо в коде и конфиге. Инвалидация удалением-на-записи гоняется под конкурентностью — медленный читатель записывает старое значение обратно после delete, и без TTL оно не восстанавливается — поэтому используешь версионированные ключи (и TTL-подстраховку). Математика доли промахов (чтения БД = total × (1 − hit_ratio)) показывает, почему 99%→90% попаданий это 10× всплеск нагрузки БД: база чувствует сторону промахов, размеренная под ~1%. Дефолт noeviction заставляет кэш Redis валить записи под давлением памяти вместо сброса холодных ключей, так что чистый кэш хочет allkeys-lru/lfu. А путь чтения горячего ключа без коалесцирования штурмует бэкенд при каждом истечении — тысячи одновременных пересчётов одного значения — чинится single-flight (один пересчёт, остальные ждут) и опционально вероятностным ранним пересчётом. Senior-привычка — читать механику, предсказывать поведение под нагрузкой и конкурентностью и выбирать структурную починку, что код поддерживает — а не ту (бо́льший TTL, больше узлов, быстрее рендер), что лишь её замазывает.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.