<? phpukraine СТАТТІ
Пошук по платформі
WORDPRESS 4 вересня 2026 · 8 хв читання

Persistent object cache у WordPress: коли Redis рятує, а коли ні

Redis для WordPress продають як універсальне пришвидшення, але object cache працює лише там, де сторінка справді ходить у базу по одні й ті самі об'єкти. Розбираємо, що саме кешує `wp_cache_*`, чому за page cache виграш може бути нульовим, як рахувати вартість round-trip, чим небезпечна евікція для сесій WooCommerce і як писати код, що коректно поводиться і з persistent, і з per-request кешем.

РP
Редакція phpukraine
Редакція платформи

WordPress має object cache завжди. Питання лише в тому, скільки він живе. За замовчуванням WP_Object_Cache — це масив у пам'яті PHP-процесу: він наповнюється на початку запиту, обслуговує повторні звернення всередині цього ж запиту й помирає разом із ним. Persistent object cache — це drop-in wp-content/object-cache.php, який підміняє клас і кладе значення в зовнішнє сховище (Redis, Memcached, Relay), щоб вони пережили запит.

Звідси й межа корисності: Redis допомагає рівно там, де наступний запит витягне з кешу те, що попередній щойно прочитав з бази. Якщо такої повторюваності немає, ви просто міняєте один round-trip на інший.

Що саме проходить через object cache

Ядро само кешує через wp_cache_* майже всі свої читання: пости і їхні мета (posts, post_meta), терміни (terms, term_meta, term_relationships), користувачів (users, useremail, user_meta), опції (options), налаштування мережі (site-options). Плюс два неочевидні шари:

  • alloptions — усі автозавантажувані опції складені в одне значення кешу. На запит це один GET замість SELECT ... WHERE autoload = 'yes'. Але якщо alloptions розрісся до сотень кілобайт, ви тягнете й десеріалізуєте цей блоб на кожному запиті, включно з тими, де з нього потрібні три ключі. Персистентний кеш тут не лікує причину, а іноді робить гірше: до вартості SQL додається вартість передачі по мережі. Поруч живе notoptions — список ключів, яких у таблиці немає, щоб не бити в базу повторно.
  • Transients. set_transient() перевіряє wp_using_ext_object_cache() і, якщо drop-in активний, пише в групу transient замість таблиці options. Це і плюс (немає розпухання options, немає autoload-сміття), і пастка: транзієнт більше не гарантовано переживе флаш або евікцію.

Починаючи з WordPress 6.1 WP_Query кешує ще й результати самих SQL-запитів (не лише об'єкти постів), тому персистентний кеш почав окупатися на архівах і кастомних лупах помітно краще, ніж на старих версіях.

А ось чого в object cache немає: згенерованого HTML. Object cache — не page cache. Він скорочує кількість запитів до MySQL, але PHP усе одно виконує весь шаблон, усі хуки і весь рендер.

Коли Redis справді рятує

Реальні сценарії, де виграш стабільний і вимірюваний:

Багато PHP-воркерів або кілька серверів застосунку. Кожен PHP-FPM процес має власний per-request кеш, і між ними немає нічого спільного. Спільний Redis перетворює N незалежних холодних кешів на один теплий. Чим більше воркерів, тим більший ефект.

Трафік, який не можна віддати page cache. Залогінені користувачі, кошик і checkout WooCommerce, адмінка, персоналізовані сторінки. Тут кожен запит — це повний цикл PHP + SQL, і саме тут object cache знімає найбільше.

Сторінки з великою кількістю дрібних об'єктів. Каталог із фасетами, сторінка товару з варіаціями і атрибутами, меню на кілька рівнів, віджети з термінами. Кожен get_post_meta() і get_term() — це або GET у Redis, або запит у MySQL.

Адмінка та WP-Cron. Списки постів, підрахунки, перевірки оновлень — дуже «балакучі» до бази місця.

Критерій, за яким варто вирішувати, — не «а раптом стане швидше», а два числа з вашого ж сайту. Перше: скільки SQL-запитів робить сторінка (define('SAVEQUERIES', true) і панель Query Monitor, або $wpdb->num_queries). Друге: скільки з них лишається після ввімкнення drop-in. Якщо було 180 запитів і стало 40 — вимикати Redis немає сенсу. Якщо було 24 і стало 19 — ви щойно додали залежність заради нічого.

Коли не рятує і коли шкодить

Анонімний трафік уже за page cache. Якщо nginx fastcgi_cache, Varnish або CDN віддають HTML без PHP, object cache впливає лише на промахи page cache і на прогрів. Виграш є, але він не там, де його очікують побачити в графіках.

Латентність каналу. Object cache — це не один запит, а сотні дрібних. Арифметика проста: час = кількість команд × RTT. Виміряйте RTT чесно — redis-cli --latency -h <host> — і порівняйте з латентністю MySQL до того ж хоста. Redis на іншій зоні доступності може виявитися дорожчим за локальну базу. Практичні висновки: Unix-сокет замість TCP, якщо Redis на тому ж хості (WP_REDIS_PATH), і батчинг замість циклів — wp_cache_get_multiple() (з WP 5.5), wp_cache_set_multiple() / wp_cache_delete_multiple() (з 5.8). Прогрів мета через update_postmeta_cache() / update_object_term_cache() дешевший за N окремих GET.

Серіалізація великих значень. Кожен set — це serialize() + передача + unserialize() на читанні. Кешувати об'єкт на кілька мегабайтів у Redis майже завжди повільніше, ніж перерахувати його. WP_REDIS_IGBINARY (за наявності розширення igbinary) зменшує обсяг, але не змінює природу проблеми.

Евікція. Найнеприємніший клас багів. Якщо maxmemory-policy = allkeys-lru, Redis мовчки викидає ключі під тиском пам'яті — і разом із кешем постів вилітають транзієнти-локи та сесії WooCommerce, якщо група wc_session_id не позначена як non-persistent. Якщо ж поставити noeviction, при досягненні maxmemory записи почнуть падати з помилкою. Правильна конфігурація: окремий інстанс або принаймні окрема база під object cache з allkeys-lru, і жодних черг, брокерів чи сесій у тому самому просторі ключів. Перевіряйте INFO statsevicted_keys, keyspace_hits, keyspace_misses.

Флаш. wp_cache_flush() у типовому drop-in — це FLUSHDB. Один деплой або один плагін, що «скидає кеш про всяк випадок», вимиває сховище цілком. У Redis Object Cache для цього є WP_REDIS_SELECTIVE_FLUSH, який видаляє лише ключі з вашим префіксом.

Точка відмови. Redis лежить — сайт має працювати. Перевірте, що drop-in при помилці підключення падає назад на in-memory кеш, а не в фатал, і що є спосіб вимкнути його без правки файлів (WP_REDIS_DISABLED).

Конфігурація без сюрпризів

// wp-config.php
define('WP_REDIS_CLIENT', 'phpredis');      // phpredis|predis|relay
define('WP_REDIS_PATH', '/run/redis/wp.sock'); // або WP_REDIS_HOST + WP_REDIS_PORT
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_PREFIX', 'site1:');        // ізоляція інстансів у спільному Redis
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);
define('WP_REDIS_MAXTTL', 86400);           // страховка від «вічних» ключів
define('WP_REDIS_IGBINARY', true);
define('WP_CACHE_KEY_SALT', 'site1');       // окремий простір для transients-ключів

Таймаути в одну секунду — свідомий вибір: краще піти в базу, ніж тримати PHP-воркер у блокуючому читанні. WP_REDIS_MAXTTL рятує від ситуації, коли ключ без TTL лежить роками і виштовхує корисні дані.

На боці Redis мінімум — maxmemory (з запасом на фрагментацію), maxmemory-policy allkeys-lru, save "" (персистентність кешу на диск не потрібна і коштує IO). У мультисайті drop-in сам префіксує ключі blog_id, крім глобальних груп.

Як писати код під персистентний кеш

Головна різниця: значення тепер може прийти з іншого процесу і бути будь-якої давнини. Тому — $found замість перевірки на false, і явна декларація груп.

add_action('init', static function (): void {
    // Спільні для всієї мережі: без префікса blog_id
    wp_cache_add_global_groups(['myplugin_licenses']);
    // Ніколи не потрапляють у Redis: живуть лише в межах запиту
    wp_cache_add_non_persistent_groups(['myplugin_runtime']);
});

function myplugin_get_report(int $user_id): array
{
    $found = false;
    $report = wp_cache_get($user_id, 'myplugin_reports', false, $found);

    if ($found) {
        return $report;   // порожній масив теж валідний результат
    }

    // Захист від cache stampede: атомарний ADD як лок
    if (! wp_cache_add("lock:{$user_id}", 1, 'myplugin_reports', 30)) {
        return myplugin_stale_report($user_id);
    }

    $report = myplugin_build_report($user_id);

    wp_cache_set($user_id, $report, 'myplugin_reports', 300);
    wp_cache_delete("lock:{$user_id}", 'myplugin_reports');

    return $report;
}

wp_cache_add() тут не випадковий: він атомарний і повертає false, якщо ключ уже є, тож із десяти паралельних промахів рахувати піде один. У per-request кеші лок теж працює — просто в межах одного процесу.

Інвалідацію групою робіть через перевірку можливостей, бо не кожен drop-in її вміє:

if (function_exists('wp_cache_supports') && wp_cache_supports('flush_group')) {
    wp_cache_flush_group('myplugin_reports');   // WP 6.1+
} else {
    foreach ($ids as $id) {
        wp_cache_delete($id, 'myplugin_reports');
    }
}

І кілька правил, які економлять години дебагу:

  • TTL обов'язковий для всього, що не має явної інвалідації. Ключ без TTL у Redis — це витік пам'яті з відкладеним ефектом.
  • Не кешуйте об'єкти з ресурсами всередині (WP_Query цілком, підключення, замикання) — вони не переживуть серіалізацію.
  • Не кладіть у ключ дані, що змінюються щозапиту (час, REQUEST_URI з UTM) — отримаєте кеш із hit ratio, близьким до нуля, і зростання evicted_keys.
  • wp_cache_flush_runtime() (WP 6.0+) чистить лише локальний масив — корисно в довгих CLI-процесах, де пам'ять PHP росте, а спільний кеш чіпати не можна.

Порядок перевірки

Перед тим як ставити Redis: увімкніть SAVEQUERIES, зафіксуйте кількість запитів і час на 3–5 типових сторінках (головна, архів, товар, сторінка залогіненого користувача). Перевірте розмір alloptionsSELECT SUM(LENGTH(option_value)) FROM wp_options WHERE autoload = 'yes' — і почистіть автозавантаження, бо це дешевша перемога, ніж нова інфраструктура.

Після: ті самі сторінки, той самий вимір, плюс hit ratio з INFO stats і панель Object Cache у Query Monitor (кількість get/set і частка влучань). Якщо кількість SQL-запитів впала суттєво, а час відповіді — ні, ваше вузьке місце було не в базі: шукайте його в PHP-профайлері, у зовнішніх HTTP-викликах або в page cache, якого немає.

ПИШЕТЕ ПРО PHP?Опублікуйте розбір або історію з проєкту на платформіРедактор із чеклістом, редактура, авторська сторінка. Републікація з блогу отримує canonical на оригінал. Відкрити редактор →
РP
Редакція phpukraine
Редакція платформи
Матеріали, які готує команда платформи на основі власних даних: каталогу вакансій, зарплатного звіту й банку питань. Кожна цифра в них рахується з бази, а не береться з голови.
оновлено 4 вересня 2026 · ліцензія CC-BY-SA-4.0
ДАЛІ ПО ТЕМІ
ЧИТАТИ ДАЛІ
← Усі статті