SQLSTATE[HY000]: General error: 5 database is locked
#Что происходит
SQLite допускает одну запись одновременно. Если один процесс пишет, а другой пытается записать или читает в режиме журнала rollback, второй получает ошибку database is locked (код 5, SQLITE_BUSY). В PHP это исключение PDO. Частые причины: длительные транзакции, незакрытые запросы, отсутствие тайм-аута ожидания, режим журнала DELETE при высокой конкурентности, база на сетевом диске (NFS, SMB), у веб-сервера нет прав записи в каталог (SQLite создаёт временные файлы журнала рядом с базой).
#Причины
| Возможная причина | Как узнать | Что делать |
|---|---|---|
| Нет busy_timeoutПо умолчанию 0. | Ошибка сразу. | PRAGMA busy_timeout. |
| Режим DELETEЧитатели блокируют писателя. | Ошибки при нагрузке. | Режим WAL. |
| Долгие транзакцииОперации внутри. | Блокировки на секунды. | Укоротите транзакции. |
| Нет прав на каталогЖурнал. | Ошибка запись/readonly. | Права www-data. |
| База на сетевом дискеNFS. | Случайные блокировки. | Локальный диск. |
| Незакрытый запросКурсор. | Блокировка остаётся. | Закройте. |
#Порядок
- Включите режим WAL: читатели не блокируют писателяSQLite допускает множество читателей, но одновременно только одного писателя, и WAL не отменяет это ограничение. Администратору приложения сначала нужно сделать согласованную резервную копию базы через SQLite Backup API или команду
.backupпри доступном подключении. После этого в обслуживаемом локальном приложении проверьтеPRAGMA journal_mode;, затем при подходящих условиях включитеPRAGMA journal_mode=WAL;. WAL требует возможность создавать рядом с БД файлы-walи-shm; не применяйте его на неподходящей сетевой файловой системе. - Задайте busy_timeoutВ коде при открытии КАЖДОГО соединения установите ожидание блокировки: для PDO SQLite выполните
$pdo->exec('PRAGMA busy_timeout = 5000');(в миллисекундах), либо используйте соответствующую настройку драйвера. Если другой процесс держит запись слишком долго, это не устранит корень проблемы, но позволит дождаться короткой транзакции. Проверьте в журнале, исчезли ли ошибкиdatabase is locked, и измерьте реальные задержки. - Выполняйте запись короткими транзакциямиОткройте код участка, который начинает транзакцию, и проверьте, что между
BEGINиCOMMITне выполняются HTTP-запросы, ожидание пользователя или длительные вычисления. Перенесите подготовку данных до транзакции, затем быстро запишите изменения и завершите её; в блоке обработки исключения выполняйтеROLLBACK, если она активна. Это обычно полезнее, чем бесконечно увеличивать таймаут ожидания. - Используйте BEGIN IMMEDIATE для транзакций, которые будут писать, чтобыЕсли транзакция заранее знает, что будет писать, в одном месте кода используйте
BEGIN IMMEDIATE, чтобы попытаться получить право записи в начале. При занятой базе команда ожидает в пределах установленногоbusy_timeout; это не «устраняет взаимоблокировки» автоматически. Не смешивайте вручнуюBEGIN IMMEDIATEс управлением транзакциямиPDO::beginTransaction()на том же соединении: выбирайте один способ управления и аккуратно обрабатывайте ошибки. - Закрывайте курсорыПосле чтения большого набора данных убедитесь, что приложение не держит активные курсоры и подготовленные запросы дольше, чем необходимо. В PHP с PDO можно завершить чтение через
$stmt->closeCursor()и убрать ссылку на объект; для языков с явнымclose()используйте штатный способ закрытия. После изменения повторите параллельную запись в тестовой среде и убедитесь, что транзакции короткие. - Проверьте права на файл базы и каталог: веб-сервер должен писать в оба,На Linux администратор сначала выполнит
ls -ld /путь/к/каталогуиls -l /путь/к/базе.sqlite, подставив реальные пути; дополнительно проверит, под каким пользователем работает PHP-FPM/служба. В режиме WAL этот пользователь должен иметь доступ на запись к базе и каталогу для файлов-wal,-shm. Не выполняйте рекурсивныйchown -R www-dataи не устанавливайтеchmod 777: реальные пользователи и модель доступа отличаются. - Не размещайте базу на сетевом дискеЕсли файл SQLite находится на SMB/NFS или другом сетевом ресурсе, посмотрите фактический путь базы и тип файловой системы (
df -T /путь/к/базе.sqliteв Linux). Блокировки и режим WAL могут работать ненадёжно между узлами; обсудите перенос базы на локальный диск сервера или переход на PostgreSQL. Перед переносом сделайте согласованную копию и переключайте приложения с остановкой записей, чтобы не потерять последние транзакции. - Найдите процессы, которые держат базу открытойНа Linux в терминале выполните
lsof /путь/к/базе.sqlite(илиfuser), подставив реальный путь, и сопоставьте PID с известными сервисами. Команда показывает открывшие файл процессы, но не гарантирует, что именно они удерживают блокировку. Не завершайте PID командой kill без выяснения роли и текущей транзакции; сначала проверьте логи и попробуйте штатно завершить работу приложения. - Если запись очень частая, ставьте её в очередь или рассмотрите PostgreSQLЕсли несколько процессов часто записывают в одну базу и при включённом WAL/коротких транзакциях очередь ожидания остаётся, организуйте одного обработчика записи (очередь сообщений или задания в базе). Для высокой конкурентности рассмотрите серверную СУБД PostgreSQL; миграция потребует проверки схемы, транзакций и резервных копий. Не пытайтесь лечить постоянную перегрузку частыми
wal_checkpoint(TRUNCATE), которые могут сами конфликтовать с работающими соединениями. - Команды для отдельных ситуацийВыберите только подходящий вариант. Это не одна последовательность: часть команд диагностирует проблему, а часть меняет настройки или данные.
#Типичный сценарий на сайте
Если два PHP-запроса одновременно записывают счётчики или логи, один из них упадёт с блокировкой. Решение: WAL, busy_timeout 5000 мс и короткие транзакции. Если запись идёт непрерывно (сотни операций в секунду), сгруппируйте записи пакетами или перейдите на PostgreSQL или MySQL.