Если у вас сайт с авторизацией, на каждого пользователя приходится сессия. По умолчанию PHP защищает эти сессии от гонок блокировкой: первый запрос захватывает lock, а остальные, пришедшие параллельно из того же браузера, ждут. Это снижает риск повредить данные, но может убивать производительность: любой долгий запрос превращает остальные во «виселиц». Разберёмся, как устроен механизм, где он особенно мешает и как безопасно сократить или убрать стопор параллельных запросов.
Почему запросы блокируются: что делает PHP под капотом
Сессия — это сериализованная структура данных, привязанная к cookie. При session_start() PHP:
- находит хранилище по
session.save_handlerиsession.save_path; - читает данные;
- устанавливает эксклюзивную блокировку на запись (в файловом обработчике это advisory lock, в Redis/Memcached — логика блокировок в расширениях);
- держит блокировку до
session_write_close()или завершения запроса.
Идея проста: пока один процесс изменяет сессию, второй ждёт, чтобы не записать поверх. Проблема — критическая секция часто охватывает весь запрос, включая вызовы внешних API, SQL и рендеринг.
Блокировка сессии защищает от гонок, но не обязана занимать 100% времени запроса. Чем меньше критическая секция, тем больше реальной параллельности.
Где чаще всего «стопорит»
Реальные сценарии, где блокировки выстреливают чаще всего:
- SPA/лендинг с десятком AJAX‑запросов после загрузки: один из них делает долгий SQL, все остальные ждут.
- Мультизагрузка файлов: прогресс‑эндпоинт и обработчик загрузки бодаются за один и тот же lock.
- Генерация отчётов, экспорт в XLSX/PDF, запись в удалённые сервисы — медленные операции под открытой сессией.
- Сторонние SDK, которые автоматически вызывают
session_start()«на всякий случай», даже если вам сессия не нужна.

Базовый рецепт: сокращаем критическую секцию
Золотое правило: читать — рано, писать — поздно. Суть в том, чтобы как можно раньше отпустить lock, если запись не требуется, и держать его минимально в тех местах, где она нужна.
1) Read‑only обработка: session_read_and_close
Если эндпоинт только читает значения, не меняя их, открывайте и сразу закрывайте сессию:
<?php
session_start(['read_and_close' => true]);
$userId = $_SESSION['user_id'] ?? null;
// Дальше никаких блокировок: можно делать тяжелую работу параллельно
Эквивалентная форма — обычный session_start() и тут же session_write_close() после чтения:
<?php
session_start();
$user = $_SESSION['user'] ?? null;
session_write_close();
// Долгая логика вне сессии
Две строчки — и параллельные запросы перестают ждать друг друга.
2) Модификация сессии: минимизируем время под lock
Классический анти‑паттерн — открыть сессию в самом начале и держать до конца. Гораздо безопаснее собрать все вычисления локально, а затем на короткое время открыть сессию, слить изменения и сразу закрыть:
<?php
// 1) Выполняем тяжелую логику без сессии
$cartDelta = computeCartDelta($request);
// 2) Открываем, быстро применяем изменения, закрываем
session_start();
$cart = $_SESSION['cart'] ?? [];
$cart = applyDelta($cart, $cartDelta);
$_SESSION['cart'] = $cart;
session_write_close();
// 3) Всё остальное уже параллелится
renderResponse($cart);
Так вы не теряете данные и не держите остальных в очереди.
3) Двойная запись? Только с re‑read
Если в процессе долгой операции вам всё же нужно записать что‑то «по этапам», не держите lock весь запрос. Закрывайте сессию и, когда понадобится снова записать, обязательно перечитывайте текущее состояние и аккуратно сливайте изменения:
<?php
// Фаза 1: читаем и закрываем
session_start();
$profile = $_SESSION['profile'] ?? [];
session_write_close();
$updated = doHeavyWork($profile);
// Фаза 2: коротко открываем, перечитываем, мержим, закрываем
session_start();
$current = $_SESSION['profile'] ?? [];
$_SESSION['profile'] = array_merge($current, $updated);
session_write_close();
Ключевой момент — «re‑read and merge», иначе вы рискуете перезаписать изменения, сделанные другим параллельным запросом.
Не вешайте всё на сессию: вынос состояния
Сессия — это удобный кэш на пользователя, но не место для всего. Чем больше вы в неё пишете, тем чаще запросы будут конфликтовать. Подумайте о переносе «горячих» структур:
- Корзина/черновики: хранить в базе или Redis по ключу пользователя, а в сессии — только идентификатор.
- Флаги прогресса/статусы: отдельный ключ в Redis или таблица, обновляемая из фонового воркера.
- Csrf/nonce/flash‑сообщения — оставляйте в сессии, это короткие и редкие записи.
Снижение количества записей в сессию напрямую повышает параллелизм.
Выбор хранилища и настройки
Файловые сессии — самый простой старт, но не лучший друг высокой конкуренции. С ростом нагрузки лучше мигрировать на in‑memory хранилища с корректной блокировкой. Если у вас проект на виртуальный хостинг, чаще всего доступен Memcached «из коробки». Для собственного Redis с тонкой конфигурацией оптимально взять VDS и поднять сервис локально.
Файлы: просто, но узкое место
Дефолтный обработчик (session.save_handler=files) использует файловые блокировки, из‑за чего параллельные запросы к одной сессии последовательно «вплотную» ждут завершения. Плюсы — простота, минусы — скорость диска, особенно на сетевых FS и при большом количестве мелких файлов.
Что настроить:
session.gc_probability/session.gc_divisor/session.gc_maxlifetime— убирать мусор, чтобы не разрасталась директория сессий.- Разнос по поддиректориям через
session.save_pathс уровнем вложенности, если файлов очень много. - Не хранить сессии на NFS/FTP‑FS: блокировки и задержки непредсказуемы.
Redis: быстро и с гибкой блокировкой
При использовании phpredis можно включить и настроить встроенный механизм блокировок. Многие ищут параметр как «session.locking», но корректные имена — в пространстве redis.session.*.
; php.ini / pool.d/*.ini
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?database=2&prefix=PHPSESSID:"
; Важное
redis.session.locking_enabled = 1
redis.session.lock_retries = 10
redis.session.lock_wait_time = 2000
; Дополнительно
redis.session.serializer = igbinary
; redis.session.compress = 1
Пояснения:
redis.session.locking_enabled— включает блокировки на уровне расширения.redis.session.lock_wait_time— ожидание между попытками (в микросекундах), помогает не крутить CPU в цикле.redis.session.lock_retries— сколько раз пытаться взять lock.redis.session.serializer— экономия CPU/памяти за счёт более компактной сериализации (например, igbinary).
Даже с Redis главный выигрыш даёт сокращение времени под lock в приложении за счёт session_write_close() и «поздних» записей. Детальнее об этом — в материале Redis для PHP‑сессий: подключение и объектный кэш.

Memcached: тоже вариант с локами
Расширение memcached предоставляет схожие настройки сессий и блокировок:
; php.ini
session.save_handler = memcached
session.save_path = "127.0.0.1:11211?weight=1&prefix=PHPSESSID:"
memcached.sess_locking = 1
memcached.sess_lock_wait = 20000
memcached.sess_lock_retries = 10
Важно: Memcached — volatile, при рестарте данные пропадут. Для критичного состояния используйте Redis или внешний персистентный стор. Сравнение и рекомендации смотрите в статье Memcached vs Redis в PHP‑кэше: сравнение и настройка.
Практические приёмы в коде
Стартуйте сессию только когда она реально нужна
Не вызывайте session_start() в bootstrap по умолчанию. Условие «нужна ли сессия?» можно строить по факту наличия cookie или по типу эндпоинта. Чем меньше запросов трогают сессию, тем меньше очередей.
Флаг session.lazy_write
Включите ленивую запись, чтобы PHP не писал сессию, если вы данные не меняли:
; php.ini
session.lazy_write = 1
На нагруженных проектах это снижает I/O и уменьшает окна блокировок.
Не держите lock во время загрузок и внешних вызовов
Перед долгими операциями (HTTP к сторонним сервисам, генерация PDF/изображений, медленные SQL) закрывайте сессию. Исключение — если вы действительно должны синхронизировать состояние сессии именно в этот момент, что встречается крайне редко.
Загрузки и upload‑progress
Механизм прогресса загрузки умеет использовать сессию. Если вы им не пользуетесь, выключите, чтобы не плодить лишние обращения:
; php.ini
session.upload_progress.enabled = 0
Фоновая запись и «транзакции»
Если логика сложная и параллельные обновления неизбежны, вынесите записываемое состояние в отдельное хранилище с атомарными операциями (Redis HINCRBY/HSET), а в сессию кладите только ссылку/ключ. Это полностью снимает очереди из‑за сессий и упрощает согласованность. Для продвинутой связки см. чек‑лист по окружению VDS: Nginx + PHP‑FPM + Memcached.
Как не потерять данные при снятии блокировок
Самая частая ошибка при «ускорении» — просто отключить блокировки в драйвере или агрессивно закрывать сессию, а затем в конце запроса записывать «как было». Это порождает гонки записи: последний записавший перезатирает изменения соседа.
Безопасный подход:
- Минимизируйте секцию под lock.
- Перед повторной записью выполняйте повторное чтение и аккуратный merge.
- Избегайте крупных структур в сессии; разбивайте на мелкие независимые поля, если возможно.
- Идём к атомарным операциям вне сессии там, где поля «суммируются».
Если у вас есть два параллельных запроса, меняющих один и тот же ключ в сессии, и вы убрали блокировки — вы почти гарантированно получите потерю данных. Решение — короткий lock на момент записи + re‑read/merge или вынос данных в стор с атомарными апдейтами.
Диагностика: как увидеть, что упёрлись в session locking
Признаки в проде: всплеск времени отклика ровно на длительность одного «медленного» запроса; параллельные эндпоинты стартуют быстро, но ждут до его завершения. Проверка проста — небольшой тестовый скрипт:
<?php
$action = $_GET['a'] ?? 'read';
if ($action === 'sleep') {
session_start();
$_SESSION['t'] = microtime(true);
sleep(5);
session_write_close();
echo "slept";
} else {
session_start(['read_and_close' => true]);
echo "read ok";
}
Откройте два запроса с одинаковой сессией: один на a=sleep, второй — без параметров. Если второй ждёт 5 секунд, вы попали в стандартную блокировку. Затем добавьте read_and_close и убедитесь, что ожидание пропало.
PHP‑FPM и окружение: не лечит, но усиливает
Иногда проблема выглядит как «зависание пула». На самом деле это очередь из‑за session locking. Тюнинг PHP‑FPM (pm.max_children, pm.process_idle_timeout, request_terminate_timeout) может смягчить пики, но не устранит корень. Лечение — в коде и хранилище сессий. Тем не менее полезно:
- Следить за
slowlogиpm.status_path— долгие запросы подсказывают, где «держится» lock. - Убедиться, что сессии не лежат на медленном/общем диске.
- Давать достаточно воркеров, чтобы ожидание одного lock не вычерпывало пул.
Чек‑лист оптимизации сессий
- Стартуем сессию только там, где она нужна (по cookie/типу эндпоинта).
- Для чтения —
session_start(['read_and_close' => true])илиsession_write_close()сразу после чтения. - Для записи — «поздний» короткий lock: вычисления до, быстрая запись, немедленное
session_write_close(). - Повторные записи — только с re‑read и merge.
- Вынести горячее состояние из сессии в Redis/БД, оставив в сессии лишь ключи.
- Перейти на Redis/Memcached с корректной настройкой блокировок, если файловые сессии упираются в диск.
- Включить
session.lazy_write=1, отключить ненужныйsession.upload_progress. - Проверить сторонние SDK на предмет скрытого
session_start().
Итог
Блокировки PHP‑сессий — не зло, а страховка от гонок. Производительность падает не из‑за самих локов, а из‑за неоправданно больших критических секций. Используйте session_read_and_close и session_write_close(), переносите горячее состояние из сессии, включайте ленивую запись и настраивайте Redis/Memcached с правильными параметрами блокировок. Так вы уберёте стопор параллельных запросов и сохраните целостность данных.


