ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

Блокировки PHP‑сессий: как убрать стопор параллельных запросов и не потерять данные

Параллельные запросы в браузере часто упираются в блокировки PHP‑сессий: один запрос держит lock, остальные ждут. В материале — как устроен механизм, где он мешает, какие настройки Redis/Memcached и приёмы в PHP помогают ускорить сайт без потери данных.
Блокировки PHP‑сессий: как убрать стопор параллельных запросов и не потерять данные

Если у вас сайт с авторизацией, на каждого пользователя приходится сессия. По умолчанию 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() «на всякий случай», даже если вам сессия не нужна.

Схема: параллельные AJAX‑запросы и блокировка одной PHP‑сессии.

Базовый рецепт: сокращаем критическую секцию

Золотое правило: читать — рано, писать — поздно. Суть в том, чтобы как можно раньше отпустить 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‑сессий: подключение и объектный кэш.

Пример настроек 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‑кэше: сравнение и настройка.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Практические приёмы в коде

Стартуйте сессию только когда она реально нужна

Не вызывайте 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 не вычерпывало пул.

Чек‑лист оптимизации сессий

  1. Стартуем сессию только там, где она нужна (по cookie/типу эндпоинта).
  2. Для чтения — session_start(['read_and_close' => true]) или session_write_close() сразу после чтения.
  3. Для записи — «поздний» короткий lock: вычисления до, быстрая запись, немедленное session_write_close().
  4. Повторные записи — только с re‑read и merge.
  5. Вынести горячее состояние из сессии в Redis/БД, оставив в сессии лишь ключи.
  6. Перейти на Redis/Memcached с корректной настройкой блокировок, если файловые сессии упираются в диск.
  7. Включить session.lazy_write=1, отключить ненужный session.upload_progress.
  8. Проверить сторонние SDK на предмет скрытого session_start().

Итог

Блокировки PHP‑сессий — не зло, а страховка от гонок. Производительность падает не из‑за самих локов, а из‑за неоправданно больших критических секций. Используйте session_read_and_close и session_write_close(), переносите горячее состояние из сессии, включайте ленивую запись и настраивайте Redis/Memcached с правильными параметрами блокировок. Так вы уберёте стопор параллельных запросов и сохраните целостность данных.

Поделиться статьей

Вам будет интересно

Let’s Encrypt wildcard через DNS-01: acme.sh, TSIG для BIND9 и сценарии с NSD без ручных правок OpenAI Статья написана AI (GPT 5)

Let’s Encrypt wildcard через DNS-01: acme.sh, TSIG для BIND9 и сценарии с NSD без ручных правок

Практическая настройка DNS-01 для wildcard-сертификатов Let’s Encrypt с acme.sh: создание TSIG-ключей, update-policy в BIND9, пров ...
SSH hardening на VDS: Fail2ban vs sshguard, Match blocks и безопасные политики доступа OpenAI Статья написана AI (GPT 5)

SSH hardening на VDS: Fail2ban vs sshguard, Match blocks и безопасные политики доступа

Пошагово укрепляем SSH на VDS без лишней паранойи: готовим аварийный откат, переводим вход на ключи, запрещаем root и пароли, огра ...
Nginx: try_files, index и приоритет location — как избежать 404 и ловушек rewrite OpenAI Статья написана AI (GPT 5)

Nginx: try_files, index и приоритет location — как избежать 404 и ловушек rewrite

Разбираем, как Nginx выбирает location и что реально проверяет try_files, когда срабатывает index и где чаще всего появляются 404. ...