繁体   English   中英

处理文件和数据库插入的最快方法-Java多线程

[英]Fastest way to process a file & DB insert - Java Multi-Threading

我在多线程编码方面是全新的。

这是我的要求:我有一个包含5万-30万条记录的文件。

它是基于列的数据(4列),以空格作为分隔符。 我需要使用空间来分割行,并将记录保存在DB的4列中。

我想开发一个多线程应用程序,它将数据插入4列的H2 DB中(使用JDBC /其他方法),大约需要2秒钟。 我需要根据收到的记录数动态更改线程池大小。

我正在使用Java Swings开发桌面应用程序。 (不是基于网络的应用)

我不知道是否有更好的并发类可以更快地完成此任务。

如果不是多线程,还有其他方法吗? 或任何其他框架?

添加批处理后,大约需要5秒钟,可以处理250,000条记录:

    BufferedReader in = new BufferedReader(new FileReader(file));
    java.util.List<String[]> allLines = new ArrayList<String[]>(); // used for something else

    String sql = "insert into test (a, b, c, d)” +
            " values (?,?,?,?)";

    PreparedStatement pstmt = conn.prepareStatement(sql);
    int i=0;
    while ((line = in.readLine()) != null) {

        line = line.trim().replaceAll(" +", " ");
        String[] sp = line.split(" ");
        String msg = line.substring(line.indexOf(sp[5]));
        allLines.add(new String[]{sp[0] + " " + sp[1], sp[4], sp[5], msg});

        pstmt.setString(1, sp[0] + " " + sp[1]);
        pstmt.setString(2, sp[4]);
        pstmt.setString(3, sp[5]);
        pstmt.setString(4, msg);

        pstmt.addBatch();

        i++;

        if (i % 1000 == 0){
            pstmt.executeBatch();
            conn.commit();
        }
    }

    pstmt.executeBatch();

改善逻辑:

  • PreparedStatement实例上创建并将其用于每次插入
  • 使用批处理仅发送大包插入

这可以通过类似以下方式完成:

private PreparedStatement pstmt;

public BatchInsertion(String sql) throws SQLException{
    pstmt = conn.prepareStatement(sql)
}

public int insert(String a, String b, String c, String d) throws SQLException{
    pstmt.setString(1, a);
    pstmt.setString(2, b);
    pstmt.setString(3, c);
    pstmt.setString(4, d);

    pstmt.addBatch();
    return batchSize++;
}

public void sendBatch() throws SQLException{
    pstmt.executeBatch();
}

在那里,您只需要使用该实例管理插入,当您到达批次中的最后一个项目或说1000个项目时,将其发送。

我使用它并不是要先将其插入Collection

注意:您需要在最后关闭语句,我将在类似的类上实现AutoCloseable来做到这一点,并且您尝试使用资源是安全的。


如果您需要多线程插入。 我建议以下架构:

创建一个线程池,每个线程都有一个连接和一个批处理来插入数据。 使用一个队列进行插入以从文件中推送数据。 每个线程将获取一个值并将其添加到批处理中。

使用这种架构,您可以轻松地增加线程数。

首先,一个轻量级的BatchInsert类可以进行此运行:

class BatchInsert implements AutoCloseable {

    private int batchSize = 0;
    private final int batchLimit;

    public BatchInsert(int batchLimit) {
        this.batchLimit = batchLimit;
    }

    public void insert(String a, String b, String c, String d) {
        if (++batchSize >= batchLimit) {
            sendBatch();
        }
    }

    public void sendBatch() {
        System.out.format("Send batch with %d records%n", batchSize);
        batchSize = 0;
    }

    @Override
    public void close() {
        if (batchSize != 0) {
            sendBatch();
        }
    }
}

然后,我使用某种平衡器来提供一个队列,并且多个Thread共享同一队列。

class BalanceBatch {
    private final List<RunnableBatch> threads = new ArrayList<>();

    private Queue<String> queue = new ConcurrentLinkedQueue<>();
    private static final int BATCH_SIZE = 50_000;

    public BalanceBatch(int nbThread) {
        IntStream.range(0, nbThread).mapToObj(i -> new RunnableBatch(BATCH_SIZE, queue)).forEach(threads::add);
    }

    public void send(String value) {
        queue.add(value);
    }

    public void startAll() {
        for (RunnableBatch t : threads) {
            new Thread(t).start();
        }
    }

    public void stopAll() {
        for (RunnableBatch t : threads) {
            t.stop();
        }
    }
}

然后,我实现逻辑以读取那些可运行实例的队列。 他们的想法是读取队列并将其发送到批处理,直到队列为空并收到命令“ STOP”。

class RunnableBatch implements Runnable {

    private boolean started = true;
    private Queue<String> queue;
    private int batchLimit;

    public RunnableBatch(int batchLimit, Queue<String> queue) {
        this.batchLimit = batchLimit;
        this.queue = queue;
    }

    @Override
    public void run() {
        try (BatchInsert batch = new BatchInsert(batchLimit)) {
            while (!queue.isEmpty() || started) {
                String s = queue.poll();
                if (s == null) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {

                    }
                } else {
                    String[] values = s.split(";");
                    batch.insert(values[0], values[1], values[2], values[3]);
                }
            }
        }
    }

    public void stop() {
        started = false;
    }
}

我通过基本测试尝试过

public static void main(String[] args) throws IOException {
    createDummy("/tmp/data.txt", 25_000_000);

    BalanceBatch balance = new BalanceBatch(10);

    balance.startAll();
    try (Stream<String> stream = Files.lines(Paths.get("/tmp/data.txt"))) {
        stream.forEach(balance::send);
    } catch (Exception e1) {
        e1.printStackTrace();
    }
    balance.stopAll();
}

public static void createDummy(String file, int nbLine) throws IOException {
    Files.write(Paths.get(file), (Iterable<String>) IntStream.range(0, nbLine).mapToObj(i -> String.format("A%d;B%d;C%d;D%d", i, i, i, i))::iterator);
}

这将打印发送的每个批次,并显示最后一个批次将非常随机,因为余额不是“恒定”的。 带有10个线程(每批记录50k)的示例:

Send batch with 50000 records
...
Send batch with 50000 records
Send batch with 15830 records
Send batch with 15844 records
Send batch with 2354 records
Send batch with 14654 records
Send batch with 40181 records
Send batch with 44994 records
Send batch with 38376 records
Send batch with 17187 records
Send batch with 27047 records
Send batch with 33533 records

笔记:

警告: createDummy函数将创建一个具有25_000_000行的文件(我已对此进行了评论)。 这大约是1GB数据的文件

我将需要更多时间来进行一些基准测试,目前我没有任何数据库可用于大规模插入。


将此多线程文件阅读器与批处理混合使用,将获得良好的效果。
请注意,这可能不是多线程的最佳实现,我从来不需要研究这个主题。 我愿意提出建议/改进。

例如,我创建了一个具有300000条记录的csv文件,读取和添加到DB的时间为Take Taken = 2625 使用try OpenCSV尝试从文件中读取记录,然后将其像这样放入数据库。 当您将DB用户准备好的语句和executeBatch()放入数据库时

//try block, connection...
PreparedStatement preparedStatement = connection.prepareStatement(query);
for(int i = 0; i < recordsCount; i++){
    preparedStatement.setString(1, rec1);
    preparedStatement.setString(2, rec2);
    preparedStatement.setString(3, rec3);
    preparedStatement.setString(4, rec4);
    preparedStatement.addBatch();
    if(i%500 == 0) preparedStatement.executeBatch();
}
preparedStatement.executeBatch();

具有executeBatch()的PreparedStatement比executeQuery快,因为您没有创建很多查询。 性能示例(请参阅测试)

您的示例中的问题是您为每个值条目创建准备好的语句。

批处理执行是一个选项,但是我将使用多个值来构建一个插入语句,如下所示:

insert into data (a, b, c, d) 
values (a1, b1, c1, d1), (a2, b2, c2, d2), (a3, b3, c3, d3)...

那么您可以执行一次,仅此而已。

其他答案已经指出您应该使用批处理插入。 我认为对于最快的导入,您实际上根本不应该使用Java。

请参阅H2文档中的快速数据库导入

为了加快大量进口,请考虑暂时使用以下选项:

  • SET LOG 0 (禁用事务日志)
  • SET CACHE_SIZE (较大的缓存速度更快)
  • SET LOCK_MODE 0 (禁用锁定)
  • SET UNDO_LOG 0 (禁用会话撤消日志)

可以在数据库URL中设置这些选项: jdbc:h2:~/test;LOG=0;CACHE_SIZE=65536;LOCK_MODE=0;UNDO_LOG=0 不建议将这些选项中的大多数用于常规用途,这意味着您需要在使用后重置它们。

如果必须导入很多行,请使用PreparedStatement或CSV导入。 请注意, CREATE TABLE(...) ... AS SELECT ...CREATE TABLE(...); INSERT INTO ... SELECT ....CREATE TABLE(...); INSERT INTO ... SELECT .... CREATE TABLE(...); INSERT INTO ... SELECT ....

我从其他一些数据库知道的另一个技巧是在插入之前删除索引,并在插入之后重新创建索引。 我不知道这是否会对H2产生任何影响,但想提一下作为可能尝试的事情。

您可以使用以下方法简单地导入CSV

INSERT INTO MY_TABLE(...) SELECT * FROM CSVREAD('data.csv');

H2文档提到CREATE TABLE(...) ... AS SELECT ...; 速度更快,但是我假设您想将数据插入到现有表中,而不是创建一个新表。

为了使这种方法有效,您的数据库需要访问data.csv文件。 对于本地数据库,这是微不足道的,但如果使用远程数据库,则并非那么容易。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM