web-dev-qa-db-fra.com

Pourquoi jdbcTemplate.batchUpdate () de Spring est-il si lent?

J'essaie de trouver le moyen le plus rapide de faire un lot insérer.

J'ai essayé d'insérer plusieurs lots avec jdbcTemplate.update (String sql), où sql a été construit par StringBuilder et ressemble à:

INSERT INTO TABLE(x, y, i) VALUES(1,2,3), (1,2,3), ... , (1,2,3)

La taille des lots était exactement de 1000. J'ai inséré près de 100 lots. J'ai vérifié l'heure en utilisant StopWatch et découvert l'heure d'insertion:

min[38ms], avg[50ms], max[190ms] per batch

J'étais content mais je voulais améliorer mon code.

Après cela, j'ai essayé d'utiliser jdbcTemplate.batchUpdate de la manière suivante:

    jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
                       // ...
        }
        @Override
        public int getBatchSize() {
            return 1000;
        }
    });

où sql ressemblait

INSERT INTO TABLE(x, y, i) VALUES(1,2,3);

et j'ai été déçu! jdbcTemplate a exécuté chaque insertion unique de 1000 lignes par lots de manière séparée. Je me suis penchée sur mysql_log et y ai trouvé mille inserts. J'ai vérifié l'heure en utilisant StopWatch et découvert l'heure d'insertion:

min [900 ms], moyenne [1100 ms], max [2000 ms] par lot

Alors, quelqu'un peut-il m'expliquer pourquoi jdbcTemplate fait des insertions séparées dans cette méthode? Pourquoi le nom de la méthode est batchUpdate? Ou est-ce que j'utilise mal cette méthode?

18
user2602807

Ces paramètres dans l'URL de connexion JDBC peuvent faire une grande différence dans la vitesse des instructions batch --- selon mon expérience, ils accélèrent les choses:

? useServerPrepStmts = false & rewriteBatchedStatements = true

Voir: performances d'insertion par lots JDBC

12
teu

J'ai également rencontré le même problème avec le modèle Spring JDBC. Probablement avec Spring Batch, l'instruction a été exécutée et validée sur chaque insert ou sur des morceaux, ce qui a ralenti les choses.

J'ai remplacé le code jdbcTemplate.batchUpdate () par le code d'insertion de lot JDBC d'origine et j'ai trouvé une amélioration majeure des performances .

DataSource ds = jdbcTemplate.getDataSource();
Connection connection = ds.getConnection();
connection.setAutoCommit(false);
String sql = "insert into employee (name, city, phone) values (?, ?, ?)";
PreparedStatement ps = connection.prepareStatement(sql);
final int batchSize = 1000;
int count = 0;

for (Employee employee: employees) {

    ps.setString(1, employee.getName());
    ps.setString(2, employee.getCity());
    ps.setString(3, employee.getPhone());
    ps.addBatch();

    ++count;

    if(count % batchSize == 0 || count == employees.size()) {
        ps.executeBatch();
        ps.clearBatch(); 
    }
}

connection.commit();
ps.close();

Vérifiez également ce lien performances d'insertion par lots JDBC

10
Rakesh Soni

Utilisez simplement la transaction. Ajoutez la méthode @Transactional on.

Assurez-vous de déclarer le gestionnaire TX correct si vous utilisez plusieurs sources de données @Transactional ("dsTxManager"). J'ai un cas où l'insertion de 60000 enregistrements. Cela prend environ 15 secondes. Aucun autre Tweak:

@Transactional("myDataSourceTxManager")
public void save(...) {
...
    jdbcTemplate.batchUpdate(query, new BatchPreparedStatementSetter() {

            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                ...

            }

            @Override
            public int getBatchSize() {
                if(data == null){
                    return 0;
                }
                return data.size();
            }
        });
    }
7
Mike

Changez votre insert sql en INSERT INTO TABLE(x, y, i) VALUES(1,2,3). Le cadre crée une boucle pour vous. Par exemple:

public void insertBatch(final List<Customer> customers){

  String sql = "INSERT INTO CUSTOMER " +
    "(CUST_ID, NAME, AGE) VALUES (?, ?, ?)";

  getJdbcTemplate().batchUpdate(sql, new BatchPreparedStatementSetter() {

    @Override
    public void setValues(PreparedStatement ps, int i) throws SQLException {
        Customer customer = customers.get(i);
        ps.setLong(1, customer.getCustId());
        ps.setString(2, customer.getName());
        ps.setInt(3, customer.getAge() );
    }

    @Override
    public int getBatchSize() {
        return customers.size();
    }
  });
}

SI vous avez quelque chose comme ça. Le printemps fera quelque chose comme:

for(int i = 0; i < getBatchSize(); i++){
   execute the prepared statement with the parameters for the current iteration
}

Le framework crée d'abord PreparedStatement à partir de la requête (la variable sql), puis la méthode setValues ​​est appelée et l'instruction est exécutée. qui est répété autant de fois que vous spécifiez dans la méthode getBatchSize(). Donc, la bonne façon d'écrire l'instruction d'insertion est avec une seule clause de valeurs. Vous pouvez jeter un œil à http://docs.spring.io/spring/docs/3.0.x/reference/jdbc.html

6
Evgeni Dimitrov

J'ai trouvé un amélioration majeure définissant le tableau argTypes dans l'appel.

Dans mon cas, avec Spring 4.1.4 et Oracle 12c, pour l'insertion de 5000 lignes avec 35 champs:

jdbcTemplate.batchUpdate(insert, parameters); // Take 7 seconds

jdbcTemplate.batchUpdate(insert, parameters, argTypes); // Take 0.08 seconds!!!

Le paramètre argTypes est un tableau int où vous définissez chaque champ de cette manière:

int[] argTypes = new int[35];
argTypes[0] = Types.VARCHAR;
argTypes[1] = Types.VARCHAR;
argTypes[2] = Types.VARCHAR;
argTypes[3] = Types.DECIMAL;
argTypes[4] = Types.TIMESTAMP;
.....

J'ai débogué org\springframework\jdbc\core\JdbcTemplate.Java et j'ai constaté que la plupart du temps était consacré à essayer de connaître la nature de chaque champ, et cela a été fait pour chaque enregistrement.

J'espère que cela t'aides !

5
Carlos Cuesta

Je ne sais pas si cela fonctionnera pour vous, mais voici une méthode sans printemps que j'ai finalement utilisée. C'était beaucoup plus rapide que les différentes méthodes Spring que j'ai essayées. J'ai même essayé d'utiliser la méthode de mise à jour par lots du modèle JDBC décrite dans l'autre réponse, mais c'était même plus lent que je ne le souhaitais. Je ne sais pas quel était l'accord et les internets n'ont pas eu beaucoup de réponses non plus. Je soupçonnais que cela avait à voir avec la façon dont les commits étaient traités.

Cette approche est tout simplement JDBC utilisant les packages Java.sql et l'interface batch de PreparedStatement. C'était le moyen le plus rapide pour obtenir 24 millions d'enregistrements dans une base de données MySQL.

J'ai plus ou moins simplement constitué des collections d'objets "record", puis j'ai appelé le code ci-dessous dans une méthode qui insère par lots tous les enregistrements. La boucle qui a construit les collections était responsable de la gestion de la taille des lots.

J'essayais d'insérer 24 millions d'enregistrements dans une base de données MySQL et cela faisait environ 200 enregistrements par seconde en utilisant Spring batch. Lorsque je suis passé à cette méthode, elle est passée à environ 2500 enregistrements par seconde. donc ma charge record de 24M est passée d'un 1,5 jour théorique à environ 2,5 heures.

Créez d'abord une connexion ...

Connection conn = null;
try{
    Class.forName("com.mysql.jdbc.Driver");
    conn = DriverManager.getConnection(connectionUrl, username, password);
}catch(SQLException e){}catch(ClassNotFoundException e){}

Créez ensuite une instruction préparée et chargez-la avec des lots de valeurs pour l'insertion, puis exécutez-la en tant qu'insertion par lot unique ...

PreparedStatement ps = null;
try{
    conn.setAutoCommit(false);
    ps = conn.prepareStatement(sql); // INSERT INTO TABLE(x, y, i) VALUES(1,2,3)
    for(MyRecord record : records){
        try{
            ps.setString(1, record.getX());
            ps.setString(2, record.getY());
            ps.setString(3, record.getI());

            ps.addBatch();
        } catch (Exception e){
            ps.clearParameters();
            logger.warn("Skipping record...", e);
        }
    }

    ps.executeBatch();
    conn.commit();
} catch (SQLException e){
} finally {
    if(null != ps){
        try {ps.close();} catch (SQLException e){}
    }
}

Évidemment, j'ai supprimé la gestion des erreurs et l'objet de requête et d'enregistrement est théorique et ainsi de suite.

Edit: Puisque votre question initiale comparait l'insert en valeurs foobar (?,?,?), (?,?,?) ... ( ?,?,?) à Spring batch, voici une réponse plus directe à cela:

Il semble que votre méthode d'origine soit probablement le moyen le plus rapide d'effectuer des chargements de données en masse dans MySQL sans utiliser quelque chose comme l'approche "LOAD DATA INFILE". Une citation des documents MysQL ( http://dev.mysql.com/doc/refman/5.0/en/insert-speed.html ):

Si vous insérez plusieurs lignes du même client en même temps, utilisez des instructions INSERT avec plusieurs listes VALUES pour insérer plusieurs lignes à la fois. Ceci est considérablement plus rapide (plusieurs fois plus rapide dans certains cas) que l'utilisation d'instructions INSERT à une seule ligne distinctes.

Vous pouvez modifier la méthode batch de mise à jour du modèle Spring JDBC pour effectuer une insertion avec plusieurs VALEURS spécifiées par appel à `` setValues ​​'', mais vous devez suivre manuellement les valeurs d'index lorsque vous parcourez l'ensemble des éléments insérés. Et vous rencontreriez un cas Edge désagréable à la fin lorsque le nombre total d'éléments insérés n'est pas un multiple du nombre de listes de VALEURS que vous avez dans votre instruction préparée.

Si vous utilisez l'approche que je décris, vous pouvez faire la même chose (utiliser une instruction préparée avec plusieurs listes VALUES), puis lorsque vous arrivez à ce cas Edge à la fin, c'est un peu plus facile à gérer car vous pouvez créer et exécuter une dernière instruction avec exactement le bon nombre de listes VALUES. C'est un peu hacky, mais la plupart des choses optimisées le sont.

4
reblace

La solution donnée par @Rakesh a fonctionné pour moi. Amélioration significative des performances. Le temps précédent était de 8 min, cette solution prenant moins de 2 min.

DataSource ds = jdbcTemplate.getDataSource();
Connection connection = ds.getConnection();
connection.setAutoCommit(false);
String sql = "insert into employee (name, city, phone) values (?, ?, ?)";
PreparedStatement ps = connection.prepareStatement(sql);
final int batchSize = 1000;
int count = 0;

for (Employee employee: employees) {

    ps.setString(1, employee.getName());
    ps.setString(2, employee.getCity());
    ps.setString(3, employee.getPhone());
    ps.addBatch();

    ++count;

    if(count % batchSize == 0 || count == employees.size()) {
        ps.executeBatch();
        ps.clearBatch(); 
    }
}

connection.commit();
ps.close();
0
Pratidnya