Подтвердить что ты не робот

SQLite в многопоточном приложении Java

Я написал java-приложение, которое периодически регистрирует события в базе данных SQLite из нескольких потоков. Я заметил, что я могу вызывать ошибки SQLite "Блокировка базы данных" относительно легко, создавая небольшое количество событий одновременно. Это заставило меня написать тестовую программу, которая имитирует худшее поведение, и я был удивлен тем, насколько плохо выглядит SQLite в этом прецеденте. Приведенный ниже код просто добавляет пять записей в базу данных, сначала последовательно, чтобы получить "контрольные" значения. Затем одновременно добавляются те же пять записей.

import java.sql.*;

public class Main {
   public static void main(String[] args) throws Exception {
      Class.forName("org.sqlite.JDBC");
      Connection conn = DriverManager.getConnection("jdbc:sqlite:test.db");

      Statement stat = conn.createStatement();
      stat.executeUpdate("drop table if exists people");
      stat.executeUpdate("create table people (name, occupation)");
      conn.close();

      SqlTask tasks[] = {
         new SqlTask("Gandhi", "politics"),
         new SqlTask("Turing", "computers"),
         new SqlTask("Picaso", "artist"),
         new SqlTask("shakespeare", "writer"),
         new SqlTask("tesla", "inventor"),
      };

      System.out.println("Sequential DB access:");

      Thread threads[] = new Thread[tasks.length];
      for(int i = 0; i < tasks.length; i++)
         threads[i] = new Thread(tasks[i]);

      for(int i = 0; i < tasks.length; i++) {
         threads[i].start();
         threads[i].join();
      }

      System.out.println("Concurrent DB access:");

      for(int i = 0; i < tasks.length; i++)
         threads[i] = new Thread(tasks[i]);

      for(int i = 0; i < tasks.length; i++)
         threads[i].start();

      for(int i = 0; i < tasks.length; i++)
         threads[i].join();
   }


   private static class SqlTask implements Runnable {
      String name, occupation;

      public SqlTask(String name, String occupation) {
         this.name = name;
         this.occupation = occupation;
      }

      public void run() {
         Connection conn = null;
         PreparedStatement prep = null;
         long startTime = System.currentTimeMillis();

         try {
            try {
               conn = DriverManager.getConnection("jdbc:sqlite:test.db");
               prep = conn.prepareStatement("insert into people values (?, ?)");

               prep.setString(1, name);
               prep.setString(2, occupation);
               prep.executeUpdate();

               long duration = System.currentTimeMillis() - startTime;
               System.out.println("   SQL Insert completed: " + duration);
            }
            finally {
               if (prep != null) prep.close();
               if (conn != null) conn.close();
            }
         }
         catch(SQLException e) {
            long duration = System.currentTimeMillis() - startTime;
            System.out.print("   SQL Insert failed: " + duration);
            System.out.println(" SQLException: " + e);
         }
      }
   }
}

Вот результат, когда я запускаю этот Java-код:

 [java] Sequential DB access:
 [java]    SQL Insert completed: 132
 [java]    SQL Insert completed: 133
 [java]    SQL Insert completed: 151
 [java]    SQL Insert completed: 134
 [java]    SQL Insert completed: 125
 [java] Concurrent DB access:
 [java]    SQL Insert completed: 116
 [java]    SQL Insert completed: 1117
 [java]    SQL Insert completed: 2119
 [java]    SQL Insert failed: 3001 SQLException: java.sql.SQLException: database locked
 [java]    SQL Insert completed: 3136

Вставка 5 записей последовательно занимает около 750 миллисекунд, я ожидаю, что параллельные вставки будут занимать примерно одинаковое количество времени. Но вы можете видеть, что с учетом 3-секундного таймаута они даже не заканчиваются. Я также написал аналогичную тестовую программу на C, используя собственные вызовы библиотеки SQLite, и одновременные вставки завершены примерно в то же время, что и параллельные вставки. Так что проблема с моей java-библиотекой.

Вот результат, когда я запускаю версию C:

Sequential DB access:
  SQL Insert completed: 126 milliseconds
  SQL Insert completed: 126 milliseconds
  SQL Insert completed: 126 milliseconds
  SQL Insert completed: 125 milliseconds
  SQL Insert completed: 126 milliseconds
Concurrent DB access:
  SQL Insert completed: 117 milliseconds
  SQL Insert completed: 294 milliseconds
  SQL Insert completed: 461 milliseconds
  SQL Insert completed: 662 milliseconds
  SQL Insert completed: 862 milliseconds

Я пробовал этот код с двумя разными драйверами JDBC (http://www.zentus.com/sqlitejdbc и http://www.xerial.org/trac/Xerial/wiki/SQLiteJDBC) и оболочку sqlite4java. Каждый раз результаты были схожими. Кто-нибудь знает о библиотеке SQLite для Java, которая не имеет такого поведения?

4b9b3361

Ответ 1

Это проблема с базой библиотеки SQLite - не с какой-либо Java-оболочкой. SQLite использует блокировки на основе файловой системы для одновременной синхронизации доступа между процессами, поскольку в качестве встроенной базы данных у нее нет выделенного процесса (сервера) для планирования операций. Поскольку каждый поток вашего кода создает свое собственное подключение к базе данных, он рассматривается как отдельный процесс, синхронизация которого происходит через блокировки на основе файлов, которые значительно медленнее, чем любой другой метод синхронизации.

Кроме того, SQLite не поддерживает блокировку строк (пока?). По существу, весь файл базы данных становится заблокирован для каждой операции. Если вам повезло, и ваша файловая система поддерживает блокировки байтов, может быть возможным, чтобы несколько читателей могли одновременно обращаться к вашей базе данных, но вы не должны допускать такого поведения.

Основная библиотека SQLite по умолчанию позволяет нескольким потокам использовать одно и то же соединение одновременно без проблем. Я предполагаю, что любая нормальная JDBC-оболочка позволит это поведение и в программах Java, хотя я и не пробовал ее.

Поэтому у вас есть два решения:

  • Поделитесь одним и тем же соединением JDBC между всеми потоками.

  • Поскольку разработчики SQLite, похоже, считают, что потоки являются злыми, вам было бы лучше, если бы один поток обрабатывал всю вашу базу данных операции и сериализовать задачи БД самостоятельно, используя код Java...

Возможно, вам стоит взглянуть на этот старый вопрос - он, похоже, накопил несколько советов по повышению производительности обновления в SQLite с течением времени.

Ответ 2

Я использую одно и то же соединение для нескольких потоков. кроме того, мне пришлось синхронизировать методы db-write, иначе я все равно получаю bussy-ошибку