Кэш (или кеш) — это быстрое промежуточное хранилище, где лежит копия данных, которые скорее всего понадобятся снова. Кэширование — прием, при котором результат дорогой операции (чтения с диска, запроса к базе, загрузки по сети) сохраняют ближе к месту использования и при повторном обращении берут копию, а не идут в источник.
Содержание
- Как работает кэш: попадание и промах
- Уровни кэширования: от процессора до базы данных
- Кэш в браузере и HTTP-заголовки
- Стратегии кэширования в приложении
- Инвалидация: главная ошибка при кэшировании
- TTL и вытеснение: LRU, LFU, FIFO
- Когда кэш вредит и как почистить кэш вручную
- Выводы
- Где применяется / связь с практикой
- FAQ
Важно различать три вещи: источник (база, сервер, диск — там «правда»), кэш (копия, которая может устареть) и политику (когда копию класть, сколько хранить и когда выбрасывать). Почти все проблемы с кэшем — из-за того, что копия разошлась с источником. Ниже — уровни кэширования, стратегии записи, TTL, LRU и инвалидация с запускаемыми примерами.
Как работает кэш: попадание и промах
Любой кэш отвечает на один вопрос: есть ли у меня свежая копия по этому ключу?
- Попадание (cache hit) — копия есть и еще годна, ответ отдается быстро.
- Промах (cache miss) — копии нет или она устарела, запрос идет в источник, результат кладется в кэш.
- Доля попаданий (hit ratio) — главная метрика: если попаданий мало, кэш только добавляет лишний шаг и расход памяти.
Минимальный рабочий пример — кэш перед «медленной базой» со сроком жизни записи (TTL). Проверено на Python 3.14:
import time
DB = {"course:1": "Python Developer"} # "медленная" база
db_reads = 0
def db_get(key):
global db_reads
db_reads += 1
time.sleep(0.2) # имитация запроса к БД
return DB.get(key)
cache = {} # key -> (value, expires_at)
TTL = 60 # секунд
def get(key):
item = cache.get(key)
if item and item[1] > time.monotonic():
return item[0], "hit"
value = db_get(key) # промах: идем в источник
cache[key] = (value, time.monotonic() + TTL)
return value, "miss"
for _ in range(3):
t0 = time.perf_counter()
value, status = get("course:1")
ms = (time.perf_counter() - t0) * 1000
print(f"{status:4} {value} {ms:.0f} мс")
print("обращений к БД:", db_reads)
Результат прогона: первый вызов — промах и около 200 мс (время зависит от машины), два следующих — попадания почти за 0 мс, в базу ушел один запрос.
miss Python Developer 204 мс
hit Python Developer 0 мс
hit Python Developer 0 мс
обращений к БД: 1
Что здесь важно. Ключ course:1 определяет, какие данные лежат в записи. expires_at хранит момент истечения, а time.monotonic() не зависит от перевода системных часов. Когда TTL истечет, следующий вызов снова станет промахом и перечитает базу. Этот пример — учебный: он однопоточный и растет без ограничения размера, про это ниже.
Уровни кэширования: от процессора до базы данных
Кэш есть почти на каждом шаге пути данных. Чем ближе к потребителю, тем быстрее ответ и тем сложнее держать копию актуальной.
| Уровень | Что кэширует | Кто управляет | Как сбрасывают |
|---|---|---|---|
| Процессор (L1/L2/L3) | строки оперативной памяти | аппаратура, прозрачно для программ | автоматически, протоколы когерентности |
| Страничный кэш ОС | блоки файлов с диска | ядро ОС | вытеснение при нехватке памяти |
| Браузер | HTML, CSS, JS, картинки, шрифты | заголовки HTTP от сервера | жесткая перезагрузка, очистка данных сайта |
| CDN и прокси | ответы сервера целиком | настройки CDN + заголовки | TTL, сброс (purge) через CDN |
| Приложение | результаты запросов и вычислений | код, Redis/Memcached, память процесса | TTL, удаление ключа при записи |
| База данных | страницы таблиц и индексов в памяти | СУБД (например, shared_buffers в PostgreSQL) | вытеснение по внутренней политике |
Кэш процессора здесь упомянут для полноты картины: он работает без участия программиста, и его устройство — отдельная тема. Разработчику чаще приходится настраивать три уровня: браузер, CDN и кэш приложения.
Кэш в браузере и HTTP-заголовки
Браузер решает, можно ли взять файл из кэша, по заголовкам ответа сервера. Основные директивы Cache-Control:
| Директива | Что означает |
|---|---|
max-age=3600 |
ответ свежий, пока его возраст меньше 3600 секунд; в это время кэш отдает его, не спрашивая сервер |
no-cache |
хранить можно, но перед использованием надо сверить с сервером |
no-store |
не сохранять ни в каком кэше (для личных и чувствительных данных) |
private / public |
общим кэшам (CDN, прокси) хранить запрещено, можно только в браузере / разрешает хранить даже там, где по умолчанию нельзя (например, ответ на запрос с авторизацией) |
s-maxage=600 |
срок свежести только для общих кэшей: перекрывает max-age, браузер его не учитывает |
Частая путаница: no-cache не означает «не кэшировать». Файл сохраняется, но при каждом использовании браузер делает условный запрос с If-None-Match (если сервер прислал ETag) или If-Modified-Since (если был Last-Modified). Если файл не менялся, сервер отвечает 304 Not Modified без тела — трафик экономится, но запрос все равно идет.
Для статики обычно используют прием с версией в имени файла: app.3f9c2a.js отдают с долгим max-age, а при изменении кода меняется имя. Так долгий кэш не мешает выкатывать обновления.
Стратегии кэширования в приложении
Стратегия отвечает на вопрос, кто и когда пишет в кэш. Выбор зависит от того, что важнее: свежесть данных, скорость записи или простота.
| Стратегия | Как работает | Плюс | Риск |
|---|---|---|---|
| Cache-aside (ленивая загрузка) | приложение читает кэш, при промахе — источник, затем кладет в кэш | просто, в кэше только нужное | первый запрос медленный, возможны устаревшие данные |
| Read-through | то же, но загрузку при промахе делает сам кэш-слой | код приложения проще | нужна библиотека или сервис с такой функцией |
| Write-through | запись идет в кэш и сразу синхронно в источник | кэш согласован с источником после записи | запись медленнее, в кэш попадает и то, что не читают |
| Write-behind (write-back) | запись в кэш, в источник — позже пачкой | быстрая запись | при сбое до сброса данные теряются |
| Write-around | запись только в источник, кэш заполняется при чтении | кэш не засоряется разовыми записями | первое чтение после записи — промах |
Пример из первого раздела — это cache-aside, самая распространенная схема для веб-приложений. Когда выбирать иначе: если данные после записи сразу читают и важна согласованность — write-through; если поток записей большой, а потеря последних секунд допустима — write-behind.
Инвалидация: главная ошибка при кэшировании
Инвалидация — удаление или обновление копии, когда источник изменился. Если про нее забыть, пользователь видит старую цену, старый профиль или удаленный товар. Ниже — неверный вариант и исправление на одном сценарии:
DB = {"price:1": 100}
cache = {}
def get(key):
if key not in cache:
cache[key] = DB[key]
return cache[key]
def update_wrong(key, value):
DB[key] = value # кэш не тронут
def update_right(key, value):
DB[key] = value
cache.pop(key, None) # инвалидация: удаляем ключ
print(get("price:1")) # 100, кэш заполнен
update_wrong("price:1", 120)
print(get("price:1"), "в БД:", DB["price:1"])
update_right("price:1", 150)
print(get("price:1"), "в БД:", DB["price:1"])
Результат прогона на Python 3.14:
100
100 в БД: 120
150 в БД: 150
После update_wrong база хранит 120, а кэш продолжает отдавать 100. После update_right ключ удален, следующее чтение — промах, и приложение берет из базы актуальные 150. Удаление ключа обычно надежнее, чем запись нового значения в кэш: при двух параллельных обновлениях запись в кэш может прийти в неверном порядке.
Граница этого приема: в многопоточной или распределенной системе удаление ключа не исключает гонку полностью (чтение старого значения может успеть положить его обратно). Поэтому к инвалидации почти всегда добавляют TTL как страховку: даже если удаление не сработало, устаревшая копия проживет ограниченное время.
TTL и вытеснение: LRU, LFU, FIFO
TTL и вытеснение решают разные задачи:
- TTL (time to live) — ограничивает устаревание: запись живет не дольше заданного времени, даже если места хватает.
- Вытеснение (eviction) — ограничивает размер: когда память кончилась, политика выбирает, что выбросить.
Популярные политики вытеснения: LRU (least recently used) выбрасывает то, к чему дольше всего не обращались; LFU (least frequently used) — то, к чему обращались реже всего; FIFO — самое старое по времени добавления. Простая реализация LRU:
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return None
self.data.move_to_end(key) # отметили как свежий
return self.data[key]
def put(self, key, value):
self.data[key] = value
self.data.move_to_end(key)
if len(self.data) > self.capacity:
old, _ = self.data.popitem(last=False) # самый давний
print("вытеснен:", old)
c = LRUCache(2)
c.put("a", 1)
c.put("b", 2)
c.get("a") # a стал свежее b
c.put("c", 3) # места нет -> уходит b
print(list(c.data))
Вывод:
вытеснен: b
['a', 'c']
Ключ b добавили позже a, но вытеснен именно он: обращение get("a") сделало a недавно использованным. В этом и отличие LRU от FIFO, который выбросил бы a.
В Python для кэширования результатов функций есть готовый декоратор functools.lru_cache:
from functools import lru_cache
@lru_cache(maxsize=128)
def square(n):
print("считаю", n)
return n * n
square(4); square(4); square(5)
print(square.cache_info())
square.cache_clear()
print(square.cache_info())
считаю 4
считаю 5
CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)
CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
Второй вызов square(4) не печатает «считаю» — это попадание. Условие корректности: результат функции должен полностью определяться аргументами, а аргументы должны быть хешируемыми (список вызовет TypeError). TTL у lru_cache нет, поэтому для данных из базы он не подходит без доработки.
Когда кэш вредит и как почистить кэш вручную
Кэш не бесплатен. Типовые проблемы: устаревшие данные без инвалидации; утечка приватных данных, если личный ответ попал в общий кэш CDN (для таких ответов — private или no-store); лавина промахов (cache stampede), когда у популярного ключа истек TTL и сотни запросов одновременно идут в базу. От лавины помогают блокировка на пересчет ключа, случайный разброс TTL и отдача устаревшей копии на время обновления.
Если сайт после обновления отображается криво, обычно достаточно жесткой перезагрузки страницы: Ctrl+F5 или Ctrl+Shift+R в Windows и Linux, Cmd+Shift+R в macOS. Полная очистка в Chrome открывается сочетанием Ctrl+Shift+Delete (в macOS — Cmd+Shift+Delete) или через меню «Удалить данные о работе в браузере»: там отмечают «Изображения и другие файлы, сохраненные в кеше» и выбирают временной диапазон, например «Все время». На Android кэш отдельного приложения чистят в настройках: «Приложения» -> нужное приложение -> «Хранилище и кеш» -> «Очистить кеш» (так в стандартном Android; в оболочках производителей пункт может называться «Память» или «Хранилище»). Не путайте с кнопкой «Очистить хранилище» («Удалить данные»): она стирает все данные приложения, включая вход в аккаунт. В Windows 11 временные файлы удаляются через «Параметры» -> «Система» -> «Память» (в справке Microsoft раздел назван «Хранилище») -> «Временные файлы».
Очистка кэша не удаляет пароли и настройки, если не выбрать их отдельно, но после нее сайты и приложения первое время открываются медленнее: кэш заполняется заново.
Выводы
- Кэш — копия данных рядом с потребителем; источник истины остается в базе или на сервере, а копия может устареть.
- Кэширование работает на всех уровнях: процессор, ОС, браузер, CDN, приложение, СУБД; разработчик чаще всего настраивает HTTP-заголовки, CDN и кэш приложения.
- Стратегию выбирают по приоритету: cache-aside — просто и распространено, write-through — согласованность после записи, write-behind — скорость записи ценой риска потери.
- TTL ограничивает устаревание, политика вытеснения (LRU, LFU, FIFO) — размер; это разные механизмы, и обычно нужны оба.
- Главная ошибка — забытая инвалидация: при изменении данных ключ удаляют, а TTL оставляют как страховку.
Где применяется / связь с практикой
Освойте тему на практике
Кэширование — одна из первых тем на собеседованиях по проектированию систем: как разгрузить базу, где поставить Redis, как не отдать пользователю чужие или устаревшие данные, что делать при лавине промахов. Эти решения разбирают вместе с балансировкой, репликацией и очередями на курсе «System Design».
Освойте тему на практике
Посмотреть формат обучения и разобрать практические кейсы с преподавателями можно на бесплатных открытых уроках.
FAQ
Кэш и буфер — это одно и то же?
Нет. Буфер временно держит данные при передаче между компонентами с разной скоростью и после передачи не нужен, а кэш хранит копию, чтобы повторно отдать ее без обращения к источнику.
Почему кэш называют «кеш» и «кэш»?
Оба написания встречаются и в словарях, и в документации: в IT-текстах распространено «кэш», а в интерфейсах Google Chrome и Android используется «кеш». Слово происходит от английского cache, то есть «тайник».
Можно ли хранить в кэше пароли и токены?
Лучше нет: ответы с личными данными отдают с Cache-Control: no-store, а в кэше приложения хранят только то, что безопасно потерять или раскрыть при ошибке настройки.



