Инвалидация кэша в PHP — это не выбор TTL и сохранение ответов в Redis. Это решение о согласованности: определить, какая информация может запаздывать, на какой срок и что должно происходить при изменении исходных данных. Плохо спроектированный кэш может показать старую цену, предоставить доступ с отозванными правами или отобразить уже не существующую доступность. Слишком консервативный кэш, напротив, переносит всю нагрузку на базу данных и теряет своё назначение.
Отправная точка — рассматривать каждую запись как данные с определёнными владельцем, жизненным циклом и риском. Это позволяет продуктовым, бизнес- и техническим командам согласовать, когда потенциально устаревшее чтение приемлемо, а когда необходимо получить актуальное состояние из источника истины.
Классифицируйте данные перед кэшированием

Не все часто запрашиваемые данные следует кэшировать, и не для всех подходит один и тот же механизм. Оценивайте каждое чтение по четырём критериям: изменчивость, последствия устаревания, стоимость обращения к источнику и устойчивость к сбою кэша.
- Низкая изменчивость и низкое влияние: публичные каталоги, метаданные или нечувствительные конфигурации обычно допускают TTL в минутах или часах — в зависимости от процесса их изменения.
- Средняя изменчивость: карточки товаров, агрегаты панелей мониторинга и результаты поиска можно кэшировать, если инвалидировать их при изменении составляющих их записей.
- Высокое влияние: права доступа, балансы, лимиты, транзакционные состояния, инвентарь при подтверждении покупки и элементы контроля авторизации требуют согласованного источника истины или явной стратегии свежести с очень строгими требованиями.
- Данные с высокой стоимостью вычисления: отчёты и производные сводки могут оправдывать кэширование, даже если к ним обращаются нечасто, но должны указывать, какие сущности их инвалидируют.
Полезно разделять кэш представления и кэш решений. Отображение прежнего названия категории в течение нескольких секунд может быть допустимо. Использование устаревшей политики для авторизации операции обычно недопустимо. Для критически важных решений обращайтесь к авторитетному источнику или храните версии, которые можно проверить перед выполнением действия.
Определите владельцев, ключи и контракты свежести
Для каждой записи необходима операционная карточка. В ней должны быть указаны источник истины, ключ, потребители, максимальный TTL, событие инвалидации, поведение при недоступности Redis и функциональный или технический ответственный. Без такого контракта ключи множатся, и никто не знает, что удалять после изменения.
Используйте предсказуемые ключи с достаточной областью действия. Например, product:42 представляет конкретную сущность; tenant:8:product:42 предотвращает смешивание данных разных организаций; а dashboard:tenant:8:period:current обозначает производный результат. Не включайте секретные данные в ключи и не используйте нестабильные сериализации в качестве идентификатора.
Также рекомендуется сохранять единообразный формат значения: полезная нагрузка, версия или дата генерации и, когда это уместно, индикатор свежести. Потребитель не должен предполагать, что кэшированный ответ эквивалентен транзакционно согласованному чтению.
final class ProductCacheKey
{
public static function detail(int $tenantId, int $productId): string
{
return "tenant:{$tenantId}:product:{$productId}:v1";
}
}Суффикс схемы позволяет изменить структуру значения без необходимости находить и удалять все исторические записи. Он не заменяет инвалидацию бизнес-данных, но снижает риски при изменении формата.
Выбирайте паттерн обновления по типу чтения
Cache-aside для повторно используемых чтений
При cache-aside приложение сначала ищет ключ; при его отсутствии оно обращается к базе данных, формирует значение и сохраняет его с TTL. Этот подход прост и подходит для относительно стабильных чтений. Его ограничение очевидно: после записи кто-то должен удалить или заменить затронутые записи.
Инвалидация должна происходить после подтверждения транзакции. Удаление до commit может привести к тому, что другой процесс перестроит кэш, пока значение ещё старое. Если приложение публикует события, паттерн outbox помогает зарегистрировать изменение в той же транзакции и затем надёжно доставить команду инвалидации.
Явное обновление и версионирование
Если сущность очень часто читается, а её изменения контролируются, запись можно обновлять после подтверждения записи. Это позволяет избежать следующего cache miss. Однако процесс должен формировать в точности то же представление, которого ожидают читатели; иначе инвалидировать и перестроить обычно менее рискованно.
Версионирование ключей полезно для широких зависимостей. Вместо удаления всех списков товаров организации увеличивается значение tenant:8:products:version, а списки включают этот номер в свой ключ. Старые списки истекают самостоятельно. Такой подход сокращает массовые удаления, но требует контролировать рост числа ключей и не должен использоваться для сокрытия плохо понятой зависимости.
Контролируйте гонки и производные зависимости
Типичная гонка происходит так: чтение не находит значение в кэше и запрашивает старое значение; запись подтверждается и инвалидирует кэш; первое чтение завершается и снова сохраняет старое значение. Для чувствительных данных сочетайте инвалидацию с версией сущности или кратковременной блокировкой перестроения. Перед записью вычисленного значения проверьте, что прочитанная версия всё ещё актуальна. Если это не так, отбросьте результат и выполните чтение повторно.
Распределённые блокировки должны быть кратковременными, иметь срок истечения и не становиться единой точкой блокировки. Их задача — сократить одновременные перестроения, а не самостоятельно обеспечивать согласованность бизнес-данных. Если блокировку получить не удалось, один из вариантов — кратко подождать перестроенное значение или разрешить ограниченное прямое чтение.
Инвалидация по зависимостям требует инвентаризации. Изменение товара может повлиять на его детали, несколько списков, результаты поиска, счётчики и панель мониторинга. Изменение роли может повлиять на эффективные права пользователей и производные меню. Явно моделируйте эти отношения:
- Инвалидируйте прямую сущность по её ключу.
- Инвалидируйте или версионируйте зависящие от неё коллекции и агрегаты.
- Пересчитывайте дорогостоящие результаты асинхронно, если это допускает пользовательский опыт.
- Не путайте очистку представления с обновлением источника истины.
Когда связь нелегко перечислить, пространство версий по организации, каталогу или политике обычно безопаснее, чем попытка найти все затронутые ключи с помощью шаблонов глобального удаления.
Используйте TTL, jitter и ограничения для защиты источника
TTL — это страховочная сетка, а не единственный механизм согласованности. Даже корректно инвалидированный ключ должен истекать: возможны сбои доставки событий, ошибки развёртывания или осиротевшие записи. Выбирайте TTL по стоимости ошибки, а не только по стоимости запроса.
Добавляйте случайный jitter к TTL, чтобы тысячи ключей, созданных одновременно, не истекали одновременно. Кроме того, защищайте источник от лавины cache miss с помощью блокировки перестроения по ключу, ограничений конкурентности и квот для каждого потребителя. Для некритичных данных можно отдавать слегка просроченное значение, пока единственный процесс пересчитывает его; для прав доступа или доступности, влияющей на решения, этот приём следует исключить или ограничить явно утверждёнными сценариями.
Спроектируйте деградацию на случай сбоя Redis
Redis — это операционная зависимость, а не источник истины. Если он не отвечает, приложению нужен определённый режим деградации. Для недорогого публичного чтения можно обращаться непосредственно к базе данных с ограничениями по времени. Для дорогих запросов целесообразно применять ограничение нагрузки, сокращать набор полей, отвечать статусом временной недоступности или использовать подходящую реплику, если это предусмотрено архитектурой.
Не превращайте сбой кэша в исчерпание подключений к базе данных. Определите короткие таймауты, circuit breakers, бюджеты запросов и метрики по маршрутам. Для критичных данных предпочтительнее отклонить операцию, чем принять решение на основе прав доступа, балансов или инвентаря, свежесть которых невозможно гарантировать.
Тестируйте и наблюдайте свежесть, а не только попадания
Высокий коэффициент попаданий не доказывает корректность кэша. Инструментируйте попадания, промахи, задержку, ошибки чтения и записи, оставшийся TTL, блокировки перестроения, отправленные и неуспешные инвалидации, а также запросы и насыщение базы данных. Связывайте эти сигналы с каждым семейством ключей, а не только с Redis как глобальным сервисом.
В тестах покройте как минимум начальное чтение, последующее обновление, удаление, инвалидацию после commit, отказ кэша и гонки между читателем и писателем. Проверяйте, что пользователь теряет доступ после отзыва права, что список отражает изменение в соответствии с его контрактом свежести и что неуспешная инвалидация запускает оповещения или восстановление.
Чек-лист для существующего PHP-приложения

- Перечислите повторяющиеся чтения и классифицируйте их по риску, изменчивости и стоимости.
- Определите источник истины и максимальную допустимую устарелость для каждого типа данных.
- Документируйте ключи, TTL, зависимости, потребителей и событие инвалидации.
- Выполняйте инвалидации или обновления только после подтверждённого commit.
- Защитите от одновременных перестроений и добавьте jitter к значимым истечениям.
- Определите режим деградации при недоступности Redis, не перегружая базу данных.
- Измеряйте свежесть и инвалидации, а не только процент попаданий.
- Регулярно проверяйте ключи без владельца, чрезмерные TTL и непокрытые зависимости.
Надёжная стратегия инвалидации кэша в PHP делает видимыми свои компромиссы: что может устареть, на какой интервал, как это исправляется и что происходит при сбое компонента. Такая ясность ценнее, чем бездумное добавление кэша.



