Попытка разобраться с параллельной записью SQLite3, есть ли лучший способ сделать это?

Я использую Delphi XE2 вместе с DISQLite v3 (который в основном является портом SQLite3). Мне нравится все, что связано с SQLite3, кроме отсутствия одновременной записи, особенно в том, что я сильно полагаюсь на многопоточность в этом проекте:(

Мой профилировщик дал понять, что мне нужно что-то с этим сделать, поэтому я решил использовать этот подход:

  1. Всякий раз, когда мне нужно вставить запись в БД, вместо того, чтобы сделать вставку, я write SQL-запрос в специальном фоллере, т.е.

    WriteToFile_Inline(SPECIAL_FOLDER_PATH + '\' + GUID, FileName + '|' + IntToStr(ID) + '|' + Hash + '|' + FloatToStr(ModifDate) + '|' + ...);

  2. Я добавил timer (в главном потоке приложения), который запускается каждую минуту, анализирует эти файлы и затем вставляет запросы, используя транзакцию.

  3. Удалите эти временные файлы в конце.

В результате я получил прирост производительности на 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.

Однако, не зная ваших шаблонов доступа, трудно сказать, будет ли это быстрее, чем пакетная вставка за минуту в отдельные транзакции.

Другие вопросы по тегам