Объектный кеш в WordPress: что ревьюить в чужом коде
Самая дорогая ошибка в объектном кеше стоит один пропущенный аргумент в ключе: один покупатель видит заказы другого. Разбираем, какие замечания писать в пулл-реквест и что проверять в дропине и настройках Redis.

Самая дорогая ошибка в объектном кеше WordPress выглядит безобидно: wp_cache_set( 'cart', $items, 'shop' ). Ключ один для всех, группа персистентная, TTL пять минут — и первый же авторизованный посетитель раздаёт свою корзину всем остальным, пока ключ не истечёт. Код проходит ревью, потому что он короткий и читаемый. Ниже — набор проверок, которые ловят такие вещи до продакшена, и формулировки замечаний, которые можно копировать в пулл-реквест. Речь про WordPress 6.x и персистентный дропин (Redis или Memcached); настройка самого Redis — отдельная тема, здесь только то, что влияет на поведение кода.
Три слоя, и ревьюить их надо порознь
Когда говорят «объектный кеш не работает», обычно смешивают три разные вещи:
- Дропин
wp-content/object-cache.php— реализация, которая подменяет встроенныйWP_Object_Cache. Его обычно кладёт плагин, и в репозитории проекта его либо нет вовсе, либо он лежит форком с правками, о которых никто не помнит. - Инфраструктура — лимиты памяти, политика вытеснения, изоляция между сайтами и окружениями.
- Прикладной код плагинов и темы — вызовы
wp_cache_*, ключи, группы, инвалидация.
На ревью вам обычно приносят только третий слой. Первые два проверяются один раз при приёмке проекта, но без них замечания к коду повисают в воздухе: нет смысла спорить о TTL, если Redis настроен на noeviction и просто начнёт отдавать ошибки записи.
Ключ: первое место, где всё ломается
Промах и закешированный false — разные вещи
wp_cache_get() возвращает false и когда ключа нет, и когда в ключе честно лежит false. Классика: функция считает, что товара нет в наличии, кладёт в кеш false, и дальше каждый запрос заново бьёт в базу, потому что false === $cached трактуется как промах. Внешне всё работает, кеш просто не кеширует.
Четвёртый аргумент $found передаётся по ссылке и отвечает именно на вопрос «ключ был»:
// WordPress 6.x
$cache_key = 'stock_' . $product_id;
$found = null;
$stock = wp_cache_get( $cache_key, 'shop_stock', false, $found );
if ( false === $found ) {
// в кеше ключа нет — идём в базу
$stock = shop_fetch_stock_from_db( $product_id );
// false и 0 кешируются нормально, промах мы отличим по $found
wp_cache_set( $cache_key, $stock, 'shop_stock', 300 );
}
return $stock;
Формулировка замечания: «false здесь валидное значение, промах по false === $cached не определяется — используйте четвёртый аргумент $found».
Чего не хватает в ключе
Ключ должен содержать всё, от чего зависит значение. Проверяйте по списку, что из этого влияет на результат и попало ли в ключ:
- ID пользователя или роль — если данные приватные или зависят от прав. Это не оптимизация, это утечка.
- Язык и валюта — на проектах с WPML или Polylang один ключ на все языки даёт перевод, который «залипает».
- Параметры выборки — пагинация, сортировка, фильтры. Ключ
'products_list'для запроса сpagedозначает, что вторая страница покажет первую. - ID сайта в мультисайте — префикс по
blog_idдропин добавляет сам, но только для неглобальных групп. Если группа объявлена черезwp_cache_add_global_groups(), префикса не будет, и сайты сети начнут делить один ключ.
Ключ без контекста — это не кеш, а общая переменная, доступная всем посетителям сайта одновременно. Ревьюйте его так же строго, как SQL-запрос с подстановкой данных пользователя.
Группы: куда на самом деле попадут данные
Группа решает, переживут ли данные запрос, увидят ли их другие сайты сети и очистятся ли они при wp_cache_flush(). Три типа ведут себя по-разному, и на ревью полезно спросить автора, какой он имел в виду.
| Тип группы | Как объявляется | Живёт | Что проверять на ревью |
|---|---|---|---|
| Персистентная (по умолчанию) | никак, просто имя группы | до TTL, вытеснения или сброса | есть ли TTL и инвалидация; нет ли в значении приватных данных |
| Неперсистентная | wp_cache_add_non_persistent_groups() | один HTTP-запрос | не ждёт ли код, что данные доживут до следующего запроса |
| Глобальная | wp_cache_add_global_groups() | как персистентная, но без префикса сайта | в мультисайте: данные правда общие для сети, или это ошибка |
Отдельно смотрите, где объявление групп вообще вызывается. Если wp_cache_add_non_persistent_groups() стоит в коде, который грузится позже первых обращений к этой группе, часть значений успеет уйти в Redis, и дальше поведение будет зависеть от порядка загрузки плагинов. Такие замечания формулируются просто: «объявление группы должно выполняться раньше первого wp_cache_get по ней — перенесите в plugins_loaded или в начало файла плагина».
Инвалидация: три вопроса автору
Код, который кладёт в кеш, пишется за пять минут. Код, который вовремя оттуда удаляет, — это то, из-за чего потом ищут причину «на сайте старая цена».
- Какие события меняют данные? Для постов правильный хук —
clean_post_cache, а не толькоsave_post: он срабатывает и при изменениях, которые идут не через форму редактора. Если данные зависят от метаполей, нужныupdated_post_metaиadded_post_meta, иначе обновление цены черезupdate_post_meta()пройдёт мимо. - Как сбрасывается группа целиком?
wp_cache_flush()в обработчике сохранения поста — повод отклонить пулл-реквест: он выносит весь кеш сайта, включая опции и пользователей. Начиная с WordPress 6.1 естьwp_cache_flush_group(), но поддержку надо проверять, её даёт не каждый дропин. - Что будет при массовом импорте? Импортёры оборачивают работу в
wp_suspend_cache_invalidation( true ). Проверьте, что флаг возвращается обратно и что после импорта кеш явно прогревается или сбрасывается, иначе сайт останется с данными «до импорта».
Когда сброса по группе нет, работает версионный ключ: вы не удаляете значения, а меняете номер версии в ключе, и старые записи вытесняются сами.
// Версионирование группы: работает на любом дропине
function shop_prices_version() {
// add создаёт ключ только если его нет, поэтому гонок на старте не будет
wp_cache_add( 'prices_version', 1, 'shop_meta' );
return (int) wp_cache_get( 'prices_version', 'shop_meta' );
}
function shop_prices_invalidate() {
wp_cache_add( 'prices_version', 1, 'shop_meta' );
wp_cache_incr( 'prices_version', 1, 'shop_meta' );
}
function shop_get_price( $product_id ) {
$cache_key = sprintf( 'price_v%d_%d', shop_prices_version(), $product_id );
$found = null;
$price = wp_cache_get( $cache_key, 'shop_prices', false, $found );
if ( false === $found ) {
$price = shop_fetch_price_from_db( $product_id );
wp_cache_set( $cache_key, $price, 'shop_prices', HOUR_IN_SECONDS );
}
return $price;
}
Цена подхода: ключ в версии N остаётся в памяти Redis до истечения TTL, даже если никому не нужен. Поэтому TTL обязателен, а бессрочные записи с версионным ключом — это медленная утечка памяти.
Где это ломается под нагрузкой
Код, который проходит ревью на стейдже с одним посетителем, ведёт себя иначе при сотнях параллельных запросов.
- Одновременное истечение. Все ключи, записанные в момент прогрева с одинаковым TTL, истекают одновременно, и весь трафик уходит в базу. Лечится разбросом TTL: не фиксированные 3600, а
3600 + random_int( 0, 300 ). - Циклы вместо батчей.
wp_cache_get()в цикле по 500 товарам — это 500 сетевых round-trip к Redis. Начиная с WordPress 5.5 естьwp_cache_get_multiple(), с 6.0 —wp_cache_set_multiple()иwp_cache_delete_multiple(). Замечание: «вынесите выборку из цикла вwp_cache_get_multiple(), промахи дособерите одним запросом в базу». - Размер значения. У Memcached дефолтный лимит на элемент — 1 МБ, и запись сверх лимита молча не сохраняется. Если в кеш кладут массив объектов
WP_Postна весь каталог, это не кеш, а вызовserialize()в никуда. Кешируйте ID, а не объекты. - Политика вытеснения. При
maxmemory-policy noevictionзаполненный Redis начинает отвечать ошибкой на запись, и дропин переходит в режим «работаем без кеша» — часто без заметных сообщений. Приallkeys-lruвытесняются в том числе сессионные и критичные ключи. Что из двух хуже, зависит от проекта, но выбор должен быть осознанным. - Общая база Redis. Прод и стейдж на одном инстансе с одним префиксом ключей — это ситуация, когда сброс кеша на стейдже роняет кеш прода. У популярных дропинов префикс задаётся константой в
wp-config.php; точное имя константы смотрите в документации конкретного плагина, у Redis Object Cache и Memcached-дропина они разные.
Что мерить до и после
Выдуманные «ускорили на 40%» в тикете бесполезны. Минимальный набор, который можно снять руками:
- Число запросов к базе на страницу. Query Monitor на стейдже, одна и та же страница, авторизованный и анонимный посетитель отдельно. Если после включения объектного кеша число запросов не изменилось, кеш не используется — проверьте, что
wp_using_ext_object_cache()возвращаетtrue. - Hit rate на стороне хранилища.
redis-cli INFO stats, поляkeyspace_hitsиkeyspace_misses. Снимайте разницу за интервал, а не абсолютные значения с момента старта. - Вытеснения. В том же выводе
evicted_keys. Растущий счётчик означает, что памяти не хватает и ваши TTL ни на что не влияют. - TTFB под параллельной нагрузкой. Любой простой нагрузочный инструмент, фиксированная конкурентность, те же данные. Указывайте в тикете условия: сколько потоков, сколько товаров в каталоге, какое железо.
Без указания условий замер не воспроизводится, и через месяц спорить с ним будет невозможно.
Что делать дальше
Соберите из перечисленного короткий шаблон ревью на пять пунктов: контекст в ключе, отличие промаха от false, тип группы, точки инвалидации, TTL с разбросом. Дальше — поставьте алерт на evicted_keys и на падение hit rate: обе метрики меняются раньше, чем начинает расти время ответа. И зафиксируйте в документации проекта, какой дропин стоит, какой версии и какие константы он читает из wp-config.php — именно это теряется первым при передаче проекта.
Дропин (drop-in) — файл в wp-content, который WordPress подключает вместо части собственного кода. Для объектного кеша это object-cache.php: он целиком заменяет встроенный класс WP_Object_Cache, поэтому набор поддерживаемых функций зависит от конкретной реализации, а не от версии WordPress.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: WP_Object_Cache — WordPress Developer Resources, Redis Object Cache — репозиторий плагина
Частые вопросы
Как понять, что персистентный объектный кеш вообще подключён?
У меня WordPress 5.9, а в статье упомянуты функции из 6.0 и 6.1. Что делать?
Код правильный, а кеш всё равно не срабатывает. Где искать?
Можно ли кешировать данные конкретного пользователя в персистентной группе?
Чем опасен wp_cache_flush() в прикладном коде?
Нужен ли объектный кеш, если уже стоит кеш страниц?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.