2012-07-09 16 views
23

Sto progettando un database MySQL che deve gestire circa 600 inserimenti di righe al secondo su varie tabelle InnoDB. La mia attuale implementazione utilizza istruzioni preparate non in batch. Tuttavia, la scrittura dei colli di bottiglia del database e le dimensioni della coda aumentano nel tempo.Prestazioni di istruzioni MySQL Insert in Java: istruzioni preparate in modalità batch rispetto a un singolo inserto con più valori

L'implementazione è scritta in Java, non conosco la versione off-hand. Utilizza lo MySQL di Java connector. Devo cercare di passare a JDBC domani. Presumo che si tratti di due pacchetti di connettori diversi.

Ho letto i seguenti thread sul tema:

e dal sito di MySQL:

Le mie domande sono:

  • Qualcuno ha consigli o esperienza sulle differenze di performance che utilizzano inserti con istruzioni preparate in modalità batch vs utilizzando una singola INSERT dichiarazione con più valori.

  • Quali sono le differenze di prestazioni tra il connettore Java MySQL e JDBC. Dovrei usare l'uno o l'altro?

  • Le tabelle sono a scopo di archiviazione e vedranno ~ 90% di scrittura su ~ 10% di lettura (forse anche meno). Sto usando InnoDB. È questa la scelta giusta su MyISAM?

Grazie in anticipo per il vostro aiuto.

+0

bene mentre si utilizza l'inserimento batch, si farà questa operazione in singola transazione. In altri casi sarà necessario inserire una transazione per riga. –

+0

Forse il dba.stackexchange sarebbe stato un posto migliore per questa domanda. –

+0

+1 per la ricerca e gli sforzi che hai già fatto anche se questo è il tuo primo post. –

risposta

27

JDBC è semplicemente uno standard Java SE di accesso al database che offre le interfacce standard in modo da non siete realmente tenuti a un'implementazione JDBC specifica. Il connettore Java MySQL (Connector/J) è un'implementazione delle interfacce JDBC solo per i database MySQL. Fuori dall'esperienza, sono coinvolto in un progetto che utilizza un'enorme quantità di dati utilizzando MySQL e per lo più preferisco MyISAM per i dati che possono essere generati: consente di ottenere transazioni con prestazioni molto più elevate, ma in generale, MyISAM è più veloce, ma InnoDB è più affidabile.

Mi chiedevo per le prestazioni delle istruzioni INSERT anche un anno fa, e ho trovato il seguente vecchio codice di test nel mio ripiano del codice (scusate, è un po 'complesso e un po' fuori dalla portata della vostra domanda). Il codice che segue contiene esempi di 4 modi di inserire i dati del test:

  • singoliINSERT s;
  • batchINSERT s;
  • bulk manualeINSERT (non utilizzarlo mai - è pericoloso);
  • e infine preparato alla rinfusaINSERT).

Esso utilizza TestNG come il corridore, e usa un po 'di codice legacy personalizzato come:

  • il metodo runWithConnection() - assicura che la connessione è chiusa o rimesso al pool di connessioni dopo la richiamata viene eseguita (ma il codice seguente non utilizza la strategia affidabile della chiusura dell'istruzione - anche senza try/finally per ridurre il codice);
  • IUnsafeIn<T, E extends Throwable> - un'interfaccia di callback personalizzata per i metodi che accettano un singolo parametro ma che potenzialmente possono generare un'eccezione di tipo E, ad esempio: void handle(T argument) throws E;.
package test; 

import test.IUnsafeIn; 

import java.sql.Connection; 
import java.sql.PreparedStatement; 
import java.sql.SQLException; 

import static java.lang.String.format; 
import static java.lang.String.valueOf; 
import static java.lang.System.currentTimeMillis; 

import core.SqlBaseTest; 
import org.testng.annotations.AfterSuite; 
import org.testng.annotations.BeforeSuite; 
import org.testng.annotations.BeforeTest; 
import org.testng.annotations.Test; 

public final class InsertVsBatchInsertTest extends SqlBaseTest { 

    private static final int ITERATION_COUNT = 3000; 

    private static final String CREATE_TABLE_QUERY = "CREATE TABLE IF NOT EXISTS ttt1 (c1 INTEGER, c2 FLOAT, c3 VARCHAR(5)) ENGINE = InnoDB"; 
    private static final String DROP_TABLE_QUERY = "DROP TABLE ttt1"; 
    private static final String CLEAR_TABLE_QUERY = "DELETE FROM ttt1"; 

    private static void withinTimer(String name, Runnable runnable) { 
     final long start = currentTimeMillis(); 
     runnable.run(); 
     logStdOutF("%20s: %d ms", name, currentTimeMillis() - start); 
    } 

    @BeforeSuite 
    public void createTable() { 
     runWithConnection(new IUnsafeIn<Connection, SQLException>() { 
      @Override 
      public void handle(Connection connection) throws SQLException { 
       final PreparedStatement statement = connection.prepareStatement(CREATE_TABLE_QUERY); 
       statement.execute(); 
       statement.close(); 
      } 
     }); 
    } 

    @AfterSuite 
    public void dropTable() { 
     runWithConnection(new IUnsafeIn<Connection, SQLException>() { 
      @Override 
      public void handle(Connection connection) throws SQLException { 
       final PreparedStatement statement = connection.prepareStatement(DROP_TABLE_QUERY); 
       statement.execute(); 
       statement.close(); 
      } 
     }); 
    } 

    @BeforeTest 
    public void clearTestTable() { 
     runWithConnection(new IUnsafeIn<Connection, SQLException>() { 
      @Override 
      public void handle(Connection connection) throws SQLException { 
       final PreparedStatement statement = connection.prepareStatement(CLEAR_TABLE_QUERY); 
       statement.execute(); 
       statement.close(); 
      } 
     }); 
    } 

    @Test 
    public void run1SingleInserts() { 
     withinTimer("Single inserts", new Runnable() { 
      @Override 
      public void run() { 
       runWithConnection(new IUnsafeIn<Connection, SQLException>() { 
        @Override 
        public void handle(Connection connection) throws SQLException { 
         for (int i = 0; i < ITERATION_COUNT; i++) { 
          final PreparedStatement statement = connection.prepareStatement("INSERT INTO ttt1 (c1, c2, c3) VALUES (?, ?, ?)"); 
          statement.setInt(1, i); 
          statement.setFloat(2, i); 
          statement.setString(3, valueOf(i)); 
          statement.execute(); 
          statement.close(); 
         } 
        } 
       }); 
      } 
     }); 
    } 

    @Test 
    public void run2BatchInsert() { 
     withinTimer("Batch insert", new Runnable() { 
      @Override 
      public void run() { 
       runWithConnection(new IUnsafeIn<Connection, SQLException>() { 
        @Override 
        public void handle(Connection connection) throws SQLException { 
         final PreparedStatement statement = connection.prepareStatement("INSERT INTO ttt1 (c1, c2, c3) VALUES (?, ?, ?)"); 
         for (int i = 0; i < ITERATION_COUNT; i++) { 
          statement.setInt(1, i); 
          statement.setFloat(2, i); 
          statement.setString(3, valueOf(i)); 
          statement.addBatch(); 
         } 
         statement.executeBatch(); 
         statement.close(); 
        } 
       }); 
      } 
     }); 
    } 

    @Test 
    public void run3DirtyBulkInsert() { 
     withinTimer("Dirty bulk insert", new Runnable() { 
      @Override 
      public void run() { 
       runWithConnection(new IUnsafeIn<Connection, SQLException>() { 
        @Override 
        public void handle(Connection connection) throws SQLException { 
         final StringBuilder builder = new StringBuilder("INSERT INTO ttt1 (c1, c2, c3) VALUES "); 
         for (int i = 0; i < ITERATION_COUNT; i++) { 
          if (i != 0) { 
           builder.append(","); 
          } 
          builder.append(format("(%s, %s, '%s')", i, i, i)); 
         } 
         final String query = builder.toString(); 
         final PreparedStatement statement = connection.prepareStatement(query); 
         statement.execute(); 
         statement.close(); 
        } 
       }); 
      } 
     }); 
    } 

    @Test 
    public void run4SafeBulkInsert() { 
     withinTimer("Safe bulk insert", new Runnable() { 
      @Override 
      public void run() { 
       runWithConnection(new IUnsafeIn<Connection, SQLException>() { 
        private String getInsertPlaceholders(int placeholderCount) { 
         final StringBuilder builder = new StringBuilder("("); 
         for (int i = 0; i < placeholderCount; i++) { 
          if (i != 0) { 
           builder.append(","); 
          } 
          builder.append("?"); 
         } 
         return builder.append(")").toString(); 
        } 

        @SuppressWarnings("AssignmentToForLoopParameter") 
        @Override 
        public void handle(Connection connection) throws SQLException { 
         final int columnCount = 3; 
         final StringBuilder builder = new StringBuilder("INSERT INTO ttt1 (c1, c2, c3) VALUES "); 
         final String placeholders = getInsertPlaceholders(columnCount); 
         for (int i = 0; i < ITERATION_COUNT; i++) { 
          if (i != 0) { 
           builder.append(","); 
          } 
          builder.append(placeholders); 
         } 
         final int maxParameterIndex = ITERATION_COUNT * columnCount; 
         final String query = builder.toString(); 
         final PreparedStatement statement = connection.prepareStatement(query); 
         int valueIndex = 0; 
         for (int parameterIndex = 1; parameterIndex <= maxParameterIndex; valueIndex++) { 
          statement.setObject(parameterIndex++, valueIndex); 
          statement.setObject(parameterIndex++, valueIndex); 
          statement.setObject(parameterIndex++, valueIndex); 
         } 
         statement.execute(); 
         statement.close(); 
        } 
       }); 
      } 
     }); 
    } 

} 

Date un'occhiata a metodi annotati con l'annotazione @Test: in realtà eseguono le INSERT dichiarazioni. Inoltre si prega di dare un'occhiata al costante CREATE_TABLE_QUERY: nel codice sorgente che utilizza InnoDB produrre i seguenti risultati a mia macchina con MySQL 5.5 installato (MySQL Connector/J 5.1.12):

InnoDB 
Single inserts: 74148 ms 
Batch insert: 84370 ms 
Dirty bulk insert: 178 ms 
Safe bulk insert: 118 ms 

Se si modifica l'CREATE_TABLE_QUERY InnoDB per MyISAM, si vedrebbe significativo aumento delle prestazioni:

MyISAM 
Single inserts: 604 ms 
Batch insert: 447 ms 
Dirty bulk insert: 63 ms 
Safe bulk insert: 26 ms 

Spero che questo aiuti.

UPD:

Per il 4 ° strada è necessario correttamente personalizzare il max_allowed_packet nel mysql.ini (sezione [mysqld]) di essere grande abbastanza per sostenere veramente grandi pacchetti.

+0

Grazie per i benchmark, questa è stata la risposta più semplice che avrei potuto chiedere. Ho implementato inserti pronti in batch oggi e ha funzionato come un fascino! – Darren

+0

Prego. :) –

+4

Qualche idea sul perché l'inserimento batch sia più lento dei singoli inserimenti su InnoDB? – stracktracer

1

Avete dei trigger su una delle tabelle interessate? In caso contrario, 600 inserti al secondo non sembrano molto.

La funzionalità di inserimento in batch di JDBC emetterà la stessa istruzione più volte nella stessa transazione, mentre SQL a più valori comprimerà tutti i valori in una singola istruzione. Nel caso di un'istruzione multivalore, sarà necessario creare SQL dell'inserto in modo dinamico e questo potrebbe essere un sovraccarico in termini di più codice, più memoria, meccanismo di protezione SQL Injection ecc. Provare prima la funzionalità batch regolare, per il carico di lavoro, non dovrebbe essere un problema

Se non si ricevono i dati in lotti, prendere in considerazione la possibilità di raggrupparli prima di inserirli. Utilizziamo una coda su thread separati per implementare una disposizione Producer-Consumer. In questo tratteniamo gli inserti fino a quando non è trascorso un certo tempo o la dimensione della coda ha superato una soglia.

Nel caso in cui si desideri che il produttore venga informato di un inserimento riuscito, è necessario un po 'più di impianto idraulico.

A volte basta bloccare sul filo può essere più semplice e pratico.

if(System.currentTimeMills()-lastInsertTime>TIME_THRESHOLD || queue.size()>SIZE_THRESHOLD) { 
    lastInsertTime=System.currentTimeMills(); 
    // Insert logic 
    } else { 
    // Do nothing OR sleep for some time OR retry after some time. 
    } 
+0

Grazie per il vostro consiglio. Ho fatto delle ricerche oggi e ho creato una relazione produttore-consumatore rudimentale. Il mio elaboratore di dati funziona in un thread, aggiungendo informazioni a una coda che appartiene al thread mysql. Sembra funzionare bene. Stavo usando innodb perché c'erano alcune importanti relazioni con le chiavi straniere che avrei cercato di mantenere. Sembra che potrebbero non essere realmente necessari nello schema graduale delle cose, quindi potrei passare a myISAM domani e vedere come vanno le cose. – Darren

8

So che questo thread è piuttosto vecchio, ma ho solo pensato di dire che se si aggiunge "rewriteBatchedStatements = true" all'URL di jdbc quando si utilizza mysql, si possono ottenere enormi guadagni di prestazioni quando si utilizzano istruzioni batch.

Problemi correlati