Попытка разобраться с параллельной записью SQLite3, есть ли лучший способ сделать это?
Я использую Delphi XE2 вместе с DISQLite v3 (который в основном является портом SQLite3). Мне нравится все, что связано с SQLite3, кроме отсутствия одновременной записи, особенно в том, что я сильно полагаюсь на многопоточность в этом проекте:(
Мой профилировщик дал понять, что мне нужно что-то с этим сделать, поэтому я решил использовать этот подход:
Всякий раз, когда мне нужно вставить запись в БД, вместо того, чтобы сделать вставку, я
write
SQL-запрос в специальном фоллере, т.е.WriteToFile_Inline(SPECIAL_FOLDER_PATH + '\' + GUID, FileName + '|' + IntToStr(ID) + '|' + Hash + '|' + FloatToStr(ModifDate) + '|' + ...);
Я добавил
timer
(в главном потоке приложения), который запускается каждую минуту, анализирует эти файлы и затем вставляет запросы, используя транзакцию.Удалите эти временные файлы в конце.
В результате я получил прирост производительности на 500%. Плюс эта техника КИСЛОТА, так как я всегда могу отсканировать SPECIAL_FOLDER_PATH
после сбоя питания и выполнения вставок я нахожу.
Несмотря на хорошие результаты, я не очень доволен используемым методом (по меньшей мере, хакерским), я продолжаю думать, что если бы у меня могли быть дженерики, такие как быстрый доступ к поиску, потокобезопасный список ACID, это было бы намного чище (а возможно и быстрее?)
Поэтому мой вопрос: знаете ли вы что-нибудь подобное для Delphi XE2?
PS. Я верю, что многие из вас, читающие код выше, будут в шоке и начнут оскорблять меня в этот момент! Пожалуйста, будьте моим гостем, но если вы знаете лучший (т.е. более быстрый) подход ACID, поделитесь своими мыслями!
3 ответа
Ваша идея отправить вставки в очередь, которая будет переставлять вставки и объединять их с помощью подготовленных операторов, очень хороша. Использование таймера в основном потоке или отдельном потоке зависит от вас. Это позволит избежать любой блокировки.
Не забудьте использовать транзакцию, а затем фиксировать ее, например, через каждые 100/1000 вставок.
О высокой производительности с использованием SQLite3, см., Например, эту статью блога (и рисунок ниже):
На этом рисунке лучшая производительность (файл выключен) получается из:
PRAGMA synchronous = OFF
- Используя подготовленные заявления
- Внутри транзакции
- В режиме WAL (особенно в режиме параллелизма)
Вы также можете изменить размер страницы или размер журнала, но приведенные выше настройки являются лучшими. См. https://stackru.com/search?q=sqlite3+performance
Если вы не хотите использовать фоновый поток, убедитесь, что WAL включен, подготовьте свои операторы, используйте пакеты и перегруппируйте свой процесс, чтобы как можно скорее снять блокировку SQLite3.
Наилучшая производительность будет достигнута путем добавления уровня клиент-сервер, как мы это сделали для mORMot.
С файлами вы организовали асинхронную очередь заданий с постоянством. Это позволяет избежать one-by-one
и использовать batch
(группа записей) подход для вставки записей. Сравнение one-by-one
а также batch
:
- первая работает в режиме автоматической фиксации (вероятно) для каждой записи, вторая упаковывает пакет в одну транзакцию и дает наибольший прирост производительности.
- first подготавливает команду INSERT каждый раз, когда вам нужно вставить запись (вероятно), второй раз в пакет и дает секунду прирост значения.
Я не думаю, что параллелизм SQLite является проблемой в вашем случае (по крайней мере, не основной проблемой). Потому что в SQLite одиночная вставка сравнительно быстра и проблемы с производительностью параллелизма вы получите с высокой рабочей нагрузкой. Вероятно, аналогичные результаты вы получите с другими СУБД, такими как Oracle.
Чтобы улучшить свой batch
подход, рассмотрим следующее:
- рассмотреть возможность установить journal_mode в
WAL
и отключить режим общего кэша. - использовать фоновый поток для обработки вашей очереди. Вместо фиксированного временного интервала (1 мин), проверьте
SPECIAL_FOLDER_PATH
чаще. И если в очереди содержится более X Кбайт данных, начните обработку. Или используйте количество записей и событий в очереди, чтобы уведомить поток, что очередь должна начать обработку. - использовать Multy-запись подготовлено
INSERT
вместо одиночной записиINSERT
, Вы можете создать INSERT для 100 записей и обрабатывать данные своей очереди в одном пакете, но по 100 записей. - Рассмотрим запись / чтение двоичных значений поля вместо текстовых значений.
- рассмотреть возможность использования набора файлов с предварительно выделенным размером.
- так далее
sqlite3_busy_timeout
довольно неэффективно, потому что не возвращается сразу, когда таблица, на которой он ожидает, разблокирована.
Я бы попробовал создать критический раздел (TCriticalSection
?) защищать каждую таблицу. Если вы войдете в критическую секцию перед вставкой строки и сразу же выйдете из нее, вы создадите лучшие блокировки таблиц, чем обеспечивает SQLite.
Однако, не зная ваших шаблонов доступа, трудно сказать, будет ли это быстрее, чем пакетная вставка за минуту в отдельные транзакции.