以极高的速度提取行

时间:2014-08-16 00:11:59

标签: java sql oracle jdbc

我在Oracle中有非常大的表(数亿行,包含数字和字符串),我需要读取该表的所有内容,格式化它并写入文件或任何其他资源。 通常我的解决方案如下:

package my.odp;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.lang.Throwable;
import java.sql.*;


public class Main {
public static volatile boolean finished = false;

public static void main(final String[] args) throws InterruptedException {
    final ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(10000);
    final Thread writeWorker = new Thread("ODP Writer") {
        public void run() {
            try {
                File targetFile = new File(args[0]);
                FileWriter fileWriter = new FileWriter(targetFile);
                BufferedWriter writer = new BufferedWriter(fileWriter);
                String str;
                try {
                    while (!finished) {
                        str = queue.poll(200, TimeUnit.MILLISECONDS);
                        if (str == null) {
                            Thread.sleep(50);
                            continue;
                        }
                        writer.write(str);
                        writer.write('\n');
                    }
                } catch (InterruptedException e) {
                    writer.close();
                    return;
                }
            }
            catch (Throwable e) {
                e.printStackTrace();
                return;
            }
        }
    };

    final Thread readerThread = new Thread("ODP Reader") {
        public void run() {
            try {
                Class.forName("oracle.jdbc.OracleDriver");
                Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@//xxx.xxx.xxx.xxx:1521/orcl", "user", "pass");

                Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
                stmt.setFetchSize(500000);
                ResultSet rs = stmt.executeQuery("select * from src_schema.big_table_view");
                System.out.println("Fetching result");
                while (rs.next()) {
                    StringBuilder sb = new StringBuilder();
                    sb.append(rs.getString(1)).append('\t');//OWNER
                    sb.append(rs.getString(2)).append('\t');//OBJECT_NAME
                    sb.append(rs.getString(3)).append('\t');//SUBOBJECT_NAME
                    sb.append(rs.getLong(4)).append('\t');//OBJECT_ID
                    sb.append(rs.getLong(5)).append('\t');//DATA_OBJECT_ID
                    sb.append(rs.getString(6)).append('\t');//OBJECT_TYPE
                    sb.append(rs.getString(7)).append('\t');//CREATED
                    sb.append(rs.getString(8)).append('\t');//LAST_DDL_TIME
                    sb.append(rs.getString(9)).append('\t');//TIMESTAMP
                    sb.append(rs.getString(10)).append('\t');//STATUS
                    sb.append(rs.getString(11)).append('\t');//TEMPORARY
                    sb.append(rs.getString(12)).append('\t');//GENERATED
                    sb.append(rs.getString(13)).append('\t');//SECONDARY
                    sb.append(rs.getString(14)).append('\t');//NAMESPACE
                    sb.append(rs.getString(15));//EDITION_NAME
                    queue.put(sb.toString());
                }

                rs.close();
                stmt.close();
                conn.close();
                finished = true;
            } catch (Throwable e) {
                e.printStackTrace();
                return;
            }
        }
    };
    long startTime = System.currentTimeMillis();
    writeWorker.start();
    readerThread.start();
    System.out.println("Waiting for join..");
    writeWorker.join();
    System.out.println("Exit:"+ (System.currentTimeMillis() - startTime));
}

}

有两个线程:一个用于从结果集中获取行,另一个用于写入字符串值。测量的加载速度大约是10Mb / s,在我的情况下,我需要加快10倍。 Profiler显示最耗时的方法是

oracle.jdbc.driver.OracleResultSetImpl.getString()

oracle.net.ns.Packet.receive()

您是否有任何想法如何让jdbc更快地加载数据? 任何关于查询优化,字符串加载优化,调整JDBC驱动程序或使用另一个,直接使用oracle JDBC实现,调整Oracle的想法都表示赞赏。

更新 我汇编并列出了下面的讨论结果:

  1. 我无法访问DBMS服务器,除了连接到Oracle数据库和服务器无法连接到任何外部资源。使用服务器或远程文件系统的任何转储和提取工具都无法应用,也无法在服务器上安装和使用任何外部Java或PL / SQL例程。只有连接才能执行查询 - 这就是全部。

  2. 我使用了探查器并挖掘了Oracle JDBC驱动程序。我发现最昂贵的操作是读取数据,即Socket.read()。所有字符串字段都表示为一个char数组,并且对性能几乎没有影响。通常,我使用分析器检查整个应用程序,而Socket.read()绝对是最昂贵的操作。提取字段,构建字符串,写入数据几乎不消耗任何东西。问题仅在于阅读数据。

  3. 服务器端数据表示的任何优化都没有实际效果。连接字符串和转换时间戳对性能没有影响。

  4. 应用程序被重写为具有多个读取器线程,这些线程将准备好的数据放入写入器队列中。每个线程都有自己的连接,没有使用池,因为它们减慢了提取速度(我使用了oracle推荐的UCP池,它占用了大约10%的执行时间,所以我放弃了它)。结果集fetchSize也增加了,因为从默认值(10)切换到50000可以提高50%的性能增长。

  5. 我测试了多线程版本如何与4个读取线程一起工作,并发现增加读者数量只会减慢提取速度。 我尝试启动2个实例,其中每个实例都有两个读取器,并且两个实例都与单个实例同时工作,即双数据提取需要与单个实例相同的时间。不知道为什么会发生这种情况,但看起来oracle驱动程序有一些性能限制。具有4个独立连接的应用程序比2个具有2个连接的App实例慢。 (Profiler用于确保驱动程序的Socket.read()仍然是主要问题,所有其他部分在多线程模式下工作正常)。

  6. 我尝试使用SAS获取所有数据,并且它可以比JDBC快2倍地执行相同的提取,它们都使用单个连接到Oracle并且不能使用任何转储操作。 Oracle确保JDBC瘦驱动程序与本机驱动程序一样快。

  7. 也许Oracle有另一种方法可以通过ODBC或其他方式快速提取到远程主机?

3 个答案:

答案 0 :(得分:3)

假设您已经检查过接口,防火墙,代理等基本网络内容,以及数据库服务器的硬件元素。

选项1:

而不是:

Class.forName("oracle.jdbc.OracleDriver");
Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@//xxx.xxx.xxx.xxx:1521/orcl", "user", "pass");

尝试使用:

OracleDataSource ods = new OracleDataSource();
java.util.Properties prop = new java.util.Properties();
prop.setProperty("MinLimit", "2");
prop.setProperty("MaxLimit", "10");
String url = "jdbc:oracle:oci8:@//xxx.xxx.xxx.xxx:1521/orcl";
ods.setURL(url);
ods.setUser("USER");
ods.setPassword("PWD");
ods.setConnectionCachingEnabled(true);
ods.setConnectionCacheProperties (prop);
ods.setConnectionCacheName("ImplicitCache01");

更多详情here

选项2:Fetchsize

斯蒂芬强烈指出,fetchsize似乎太大了。

而且,对于50,000的获取大小,你的-Xms和-Xmx是多少。另外,在分析器中,最大堆大小是什么?

选项3:数据库

  • 检查src_schema.big_table_view

  • 的索引和查询计划
  • 这是一个工具还是一个应用系统。如果只是一个工具,你可以 基于数据库系统添加并行度,索引提示,分区等 功能

选项4:主题

n&lt;应用程序服务器上的核心数

您可以启动n编写器的线程,每个线程都配置为处理某个存储桶,例如thread1处理0到10000,写入n个不同的文件,一旦完成所有的文件,后加入,最好使用低级OS命令将文件合并在一起。

那就是说,所有这些都不应该像现在这样预先定义代码。 'n'和桶应该在运行时计算。并且创建的线程数量超过了系统支持的线程数量。

选项5:

而不是

select * from src_schema.big_table_view

您可以使用

SELECT column1||CHR(9)||column2||CHR(9).....||columnN FROM src_schema.big_table_view

这可以避免创建500000 StringBuildersStrings。 (假设没有涉及其他复杂格式)。 CHR(9)是制表符。

选项6:

与此同时,您还可以向DBA查询是否存在任何数据库系统问题,并使用Oracle support提出SR。

答案 1 :(得分:1)

看起来您已经找到并调整了行预取参数。但是,根据Oracle文档:

  

&#34;没有最大预取设置,但经验证据表明10是有效的。 Oracle从未发现将预取设置为高于50的性能优势。如果没有为连接设置默认的行预取​​值,则默认为10。&#34;

您将其设置为500000.尝试将其卷回到50左右......正如Oracle建议的那样。 (为什么?可能过多的预取大小导致服务器或客户端使用过多的内存来缓冲预取的数据。这可能会导致&#34;敲击效果&# 34;其他方面,导致吞吐量降低。)

参考(来自Oracle 10g文档):


通过在多个Java线程中运行同步查询(例如,在表的单独&#34;部分和#34;),将每个结果集写入单独的流/文件,您可以获得更大的吞吐量。但是你有把输出流/文件拼接在一起的问题。 (以及是否获得整体改进将取决于客户端和服务器端核心数,网络和NIC容量以及磁盘I / O容量。)

除此之外,我无法想到在Java中更快地做到这一点。但你可以尝试PL / SQL,或者更低级别的东西。 (我不是Oracle专家。请与您的DBA联系。)

Java的速度提升了10倍......雄心勃勃。

答案 2 :(得分:1)

您的分析存在缺陷

您列出的方法很可能已经过高度优化。我已经分析了在Oracle JDBC代码中StringBuffer.append()内部花费最多且时间最多的系统,因为整个系统使用PreparedStatement并且它调用了很多的方法! 。毋庸置疑,在我们的案例中,这是一个红色的鲱鱼。

描述您的网络流量:

如果你的连接已经饱和,这是你的瓶颈,而不是你列出的代码。

如果必须将Oracle作为数据源,则需要在服务器端完成。你将永远不会通过网络连接提取数亿条记录,然后再以10倍的速度再次返回,除非你在两个端点都拥有10倍的网卡,并且所有这些记录都已绑定。即便如此,我仍然怀疑你将获得10倍的吞吐量

如果您真的只限于Java和Oracle,那么获得比现在更多吞吐量的唯一方法是在服务器上run the Java as a stored procedure生成所需的文件,然后从中检索它们远程系统。

我已经构建了处理数百万个事务的系统,只需要一个网络连接即可实现这种吞吐量,它发生在专用发送/接收交换机上的多个网络接口的机器网格上子网与数据中心的其余流量隔离。

同时

您的线程代码充其量是天真的。您永远不应该手动创建和管理线程。 ExecutorService已经存在了10年,使用它! ExecutorCompletionService就是您要在这种情况下使用的actually in almost all cases

如果你能使用番石榴,

ListenableFuture是更好的选择。