【问题标题】:The performance comparison of multi-threaded inserts(updates) on database and single-threaded sequential inserts(updates)?数据库上的多线程插入(更新)和单线程顺序插入(更新)的性能比较?
【发布时间】:2017-07-31 04:58:35
【问题描述】:

让我们想象一个环境:有一个数据库客户端和一个数据库服务器。 db客户端可以是Java程序或其他等; db服务器可以是mysql、oracle等。

需求是在数据库服务器的一张表中插入大量记录。

最简单的方法是有一个循环,客户端每次在其中插入一条记录,直到插入所有记录。这是单线程顺序插入。

还有另一种多线程并发插入方式,让客户端同时启动多个线程,每个线程将一条记录插入表中。直观地说,因为这些记录是独立的,并且假设现代数据库服务器带有 RAID,其中并发 IO 得到很好的支持,它们似乎能够为多个插入获得实际和真正的并发,因此,这种方式可以改善性能,与上述方法相比。

但是,当我深入了解更多细节时,发现情况可能并非如此。这个链接——Multi threaded insert using ORM? 说在同一张表上的插入需要对整个表上的每一次写入都加锁。因此,每次插入只会阻塞另一个后续插入,最终,这种方式只是另一种顺序多次插入,根本没有性能提升。

我的问题如下:

  1. 为什么大多数 DB 都这样对待同一张表上的多线程插入?
  2. 为什么对整个表强制插入锁?
  3. 多线程更新是否被视为多线程插入?

尽管处理大量插入的最佳方法似乎是启用批量插入,但我仍然很好奇在插入发生时锁定整个表的理由。

提前致谢!

================================================ =======================

经过大量阅读和研究,这表明我的问题实际上是错误的。真正的事情是一个插入不会同时阻塞另一个插入。(至少对于 Oracle 来说是这样)。

【问题讨论】:

  • 这个问题的前提是错误的,因为链接问题中接受的答案是错误的。该线程中未接受的答案更准确。
  • 您好 Jon,感谢您的意见,这很有帮助。您是在线程中说的——stackoverflow.com/questions/6602399/…,答案是——数据库上的多线程插入语句不会真正让它执行得更快,因为在大多数数据库中,表需要锁定才能插入。 错了吗?
  • 另外你能指出哪个答案更准确吗?非常感谢。
  • 是的,这个答案是错误的。也许对于那个特定的数据库是正确的,但答案不适用于“大多数”数据库。
  • @Jon Heller 感谢您的跟进。

标签: java mysql multithreading oracle locking


【解决方案1】:

这个答案需要了解数据库,这超出了这里简单答案的范围。既然您询问了 Oracle:

Oracle 不会以您认为的方式锁定整个表。在插入过程中,对本质上是表结构的内容有一个锁定(即,有人不能在插入过程中删除列),但在数据级别,没有锁定。这意味着您可以在单个表上进行许多并发插入。更新(在 Oracle 中)是类似的。但是,在这种情况下,正在更新的数据上有一个行锁。所以你可以在同一张表上有很多并发更新;但不在同一行。

说了这么多,多线程插入不是加载大量数据的方式。为此,Oracle 提供了一种替代方法,即直接路径加载。在这种方法中,我们加载行集,而不是逐行(缓慢地)加载。并不是单次插入很慢;恰恰相反,它们非常快。但即使每次插入 0.1 毫秒,当您必须加载 1 亿行时,也就是 2.7 小时!由于基于集合的方法允许数据库执行并行性,而不是手动“本土”多线程方法 因此,为了让您了解可以做什么,我在大约 10 分钟内加载了大约 60 亿行(大约 1 TB 的数据)。 最后,数据加载通常受 CPU 限制;不受 IO 限制。

【讨论】:

  • 您好 BobC,感谢您的意见,这非常有帮助。我知道批量插入是最好的方法。但是我真的很想比较一下单线程插入和多线程插入,哪个效率更高。既然您提到 oracle 允许在同一个表上同时插入,这种并发是真正并行的,就像两个磁盘头同时写入磁盘还是只是逻辑并发 - 单头或调度写入。
  • 另外,你能不能看一下这个线程——stackoverflow.com/questions/6602399/…,你认为第一个答案中的语句——数据库上的多线程插入语句不会真正让它执行更快,因为在大多数数据库中,表需要锁定才能插入 是真还是假。非常感谢!
  • @BoYe 如果您进行常规插入,则磁盘 IO 几乎无关紧要,因为所有内容都将写入内存。对数据库文件的 IO 本质上与事务是异步的。至于比较多线程和单线程(基于数组)的插入,我的团队已经完成了这项工作。这是一个视频 tat 讨论并显示:youtube.com/watch?v=II9qxyBw23g。此外,这是一个视频,展示了手动并行与基于集合的对比。 youtube.com/watch?v=sriSU6eWGzU
  • @BoYe 关于传统插入的表锁的说法对于 Oracle 来说是不正确的。我已经在上面的回答中说明了这一点。如果 是真的,你认为甲骨文如何能够维持每秒数十万的交易率!
  • 感谢您的回答,这真的揭开了我的疑问!
【解决方案2】:

没有什么比写一个演示来证明一个理论更好的了。

我制作了以下演示来比较单线程顺序插入(无批处理)和多线程插入与 Oracle 之间的性能。

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource; 
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
  * Created by boy on 14/03/17.
*/
public class DatabasePerformanceTest {
  //constants
  private static final String DBURL ="jdbc:oracle:thin:@xxxxxxxx:1521:xxxxx";
  private static final String DBUSER = "xxx";
  private static final String DBPASS = "xxxx";
  private static final Integer INSERT_AMOUNT = 10000;
  private static final String INSERT_PERSON = "insert into Persons values(1, 'xx', 'xx', 'xxxxxxx', 'xxxxxxx')";
  //pools
  private DataSource ds;
  private ExecutorService executor;

public static void main(String[] args) throws SQLException, InterruptedException {
    DatabasePerformanceTest test = new DatabasePerformanceTest();
    test.setUp();
    long begin = System.currentTimeMillis();
    //test.insertByRowByRow();
    test.insertByMultipleThreads();
    long end = System.currentTimeMillis();
    System.out.println("Time spent:" + (end - begin) + "ms");
}

private void setUp() {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(DBURL);
    config.setUsername(DBUSER);
    config.setPassword(DBPASS);
    config.addDataSourceProperty("cachePrepStmts", "true");
    config.addDataSourceProperty("prepStmtCacheSize", "250");
    config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
    config.addDataSourceProperty("dataSourceClassName", "oracle.jdbc.driver.OracleDriver");
    ds = new HikariDataSource(config);
    this.executor = Executors.newFixedThreadPool(128);
}

private void insertOnePerson(Connection connection) throws SQLException {
    Statement statement = null;
    try {
        statement = connection.createStatement();
        statement.execute(INSERT_PERSON);
    } finally {
        try {
            if (statement != null) {
                statement.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    System.out.println("Inserting one person is done.");
}

private void insertByRowByRow() throws SQLException {
    for (int i = 0; i < INSERT_AMOUNT; i++) {
        this.insertOnePerson(ds.getConnection());
    }
}

private void insertByMultipleThreads() throws InterruptedException {
    for (int i = 0; i < INSERT_AMOUNT; i++) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    insertOnePerson(ds.getConnection());
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        });
    }
    executor.shutdown();
    executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
   }
}

经过测试,清楚地表明多线程插入比单线程序列化插入(无批处理)快4倍左右。

因此,链接的第一个答案——Multi threaded insert using ORM?是错误的。

话虽如此,就像 BobC 提到的那样,上述方法是一种“本土化”的方法,处理大量插入的最佳方法是批量插入。(加载行集)

【讨论】:

    【解决方案3】:

    最简单的方法是有一个循环,客户端每次在其中插入一条记录,直到插入所有记录。这是单线程顺序插入。

    即使是单线程操作,暂停自动提交(可以通过启动事务来完成),在批处理中插入多个条目,然后提交更改也比 1-by-1 插入效率要高得多.

    ...假设现代数据库服务器带有 RAID,其中并发 IO 得到很好的支持

    实际上,硬件层面可能没有并发 IO 这样的东西。 IO 请求可以像网络接口中的数据包一样被序列化,即使我们认为它们是与服务器的多个并发连接。然而,正是多个应用程序线程对 IO 请求的排队使 IO 总线最大化。

    顺便说一句,RAID 通常也是串行 IO,通常甚至比单个设备还要慢——尤其是当我们谈论写入时。例如,RAID5 非常慢,以至于大多数高性能集群都使用 RAID50 来尝试将性能提高到足够的水平。

    为什么大多数数据库都这样对待同一张表上的多线程插入?

    这高度依赖于数据库类型,并且可能与它如何保持表的组织有关。大多数插入写入数据表的末尾(或争夺空行),因此多个线程将满足相同的磁盘空间,从而使插入的排序变得高效。

    为什么要强制对整个表进行插入锁?

    不是。同样,这在很大程度上取决于数据库如何实现插入。

    多线程更新是否被视为多线程插入?

    我不这么认为。尽管可能会使用区域锁,但更新发生在表中的不同位置,如果索引字段被更新,索引上的锁肯定会被交叉。

    您的问题确实应该是“如何最大限度地提高插入带宽”。我(和其他人)已经提到将插入的批处理作为第一步。您还需要确保使用数据库连接池——这对于单线程数据库操作也很重要。池化意味着您可以同时使用多个连接,并且您不必为每个数据库事务创建连接。那里有许多数据库连接池库。我们使用HikariCP

    希望这会有所帮助。

    【讨论】:

    • 您的 cmets 非常有帮助!非常感谢!我知道解决我的要求的最有效方法是通过批量插入,就像我在问题的最后一段中所说的那样。话虽如此,真正让我好奇的是,为什么多线程插入的性能甚至比单线程插入更差(由其他一些线程回答)根据我的理解,这似乎违反直觉在 RAID 硬件上。
    • 如果你的说法在现实中,硬件层面可能没有并发IO这种东西。对于RAID也是如此,我觉得多线程真的是与单线程没有什么不同。
    • RAID 实际上通常比普通 IO,因为它必须协调多个物理设备的 IO。例如 RAID5 是一个性能猪,这就是为什么经常使用 RAID50 @BoYe。但是通过使用多个线程,你可以比单个线程更有效地填充这个 IO 链,因为单个线程必须与网络和处理抗衡。
    猜你喜欢
    • 1970-01-01
    • 2013-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多