2012-05-22 18 views
19

Ho scritto un'applicazione java che registra sporadicamente gli eventi in un database SQLite da più thread. Ho notato che posso innescare gli errori di "Database Locked" di SQLite relativamente facilmente, generando un piccolo numero di eventi contemporaneamente. Questo mi ha spinto a scrivere un programma di test che simula il comportamento del caso peggiore e sono rimasto sorpreso da quanto male sembra che SQLite funzioni in questo caso d'uso. Il codice pubblicato di seguito aggiunge semplicemente cinque record a un database, in primo luogo in modo sequenziale per ottenere valori di "controllo". Quindi gli stessi cinque record vengono aggiunti contemporaneamente.SQLite in un'applicazione java con multithreading

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); 
     } 
     } 
    } 
} 

Ecco l'output quando si esegue questo codice 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 

Inserimento 5 record in modo sequenziale richiede circa 750 millisecondi, ci si aspetterebbe inserti simultanei a prendere all'incirca la stessa quantità di tempo. Ma puoi vedere che dato un timeout di 3 secondi non finiscono nemmeno. Ho anche scritto un programma di test simile in C, usando le chiamate alle librerie native di SQLite e gli inserimenti simultanei finiti all'incirca nello stesso momento in cui gli inserti concorrenti facevano. Quindi il problema è con la mia libreria java.

Ecco l'output quando eseguo la versione 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 

ho provato questo codice con due diversi driver JDBC (http://www.zentus.com/sqlitejdbc e http://www.xerial.org/trac/Xerial/wiki/SQLiteJDBC), e l'involucro sqlite4java. Ogni volta i risultati erano simili. Qualcuno là fuori sa di una libreria SQLite per java che non ha questo comportamento?

risposta

23

Questo è un problema con core SQLite library - non con alcun wrapper Java. SQLite utilizza i blocchi basati su file system per la sincronizzazione simultanea degli accessi tra i processi, poiché come database incorporato non ha un processo dedicato (server) per pianificare le operazioni. Poiché ogni thread nel codice crea la propria connessione al database, viene trattato come un processo separato, con la sincronizzazione che avviene tramite blocchi basati su file, che sono significativamente più lenti di qualsiasi altro metodo di sincronizzazione.

Inoltre, SQLite non supporta il blocco per riga (ancora?). In sostanza, l'intero file di database diventa locked per ciascuna operazione. Se sei fortunato e il tuo filesystem supporta i blocchi di byte-range, potrebbe essere possibile per più lettori accedere al tuo database contemporaneamente, ma non dovresti assumere questo tipo di comportamento.

La libreria di base SQLite by default allows multiple threads to use the same connection concurrently senza problemi. Presumo che qualsiasi wrapper JDBC sane consenta questo comportamento anche nei programmi Java, anche se in realtà non l'ho provato.

Pertanto si hanno due soluzioni:

  • condividono la stessa connessione JDBC fra tutte le discussioni.

  • Dal momento che gli sviluppatori SQLite sembrano pensare che threads are evil, si sarebbe meglio avere una filo di gestire tutte le operazioni di database e serializzare compiti DB sul proprio codice Java utilizzando ...

Potresti dare un'occhiata a this old question of mine - sembra aver accumulato diversi suggerimenti sul miglioramento delle prestazioni di aggiornamento in SQLite nel tempo.

+0

Quindi, se la radice del problema è il blocco del filesystem, perché il mio codice C funziona alla stessa velocità di quello? – jlunavtgrad

+2

evviva! L'utilizzo della stessa connessione JDBC ha risolto completamente il problema. Ora vedo prestazioni migliori o uguali al mio codice C. L'ho provato con il precedente programma java ieri, ma ora mi rendo conto che stavo sovrascrivendo la mia connessione con una nuova per ogni inserto. – jlunavtgrad

+0

Vedi anche: http://stackoverflow.com/questions/24513576/opening-database-connection-with-the-sqlite-open-nomutex-flag-in-java – Stephan

1

Io uso la stessa connessione per più thread. inoltre ho dovuto sincronizzare i metodi db-write, altrimenti ho ancora un errore di battuta