典型的决策支持系统定期载入和插入大量数据。大规模决策支持系统一般都部署在分区环境中。缓冲插入是由 DB2 提供的一种编程技术,使在分区环境中插入密集型工作负载能达到最佳的性能。本文讨论如何在 Java 中利用此功能。还将分析在不同的配置(象变化的行的长度和提交频率)下使用此功能的性能增益。
典型的决策支持系统(decision support system,DSS)定期载入和插入大量数据。大规模 DSS 一般都部署于分区环境中。由于 Java™ 编程语言的普及,本文向您描述可以如何通过使用 Java 来利用 DB2® Universal Database™ 的缓冲插入选项,以使在分区环境中插入密集型(insert-intensive)工作负载达到最佳的性能。
本文假定您多少熟悉一些 Java、SQLJ 和 JDBC。请注意,本文中说明的概念只在 DB2 UDB EEE(即支持数据库分区的 UDB 版本)上有效。
JDBC 技术是一种 API,让您通过 Java 编程语言能实质地访问任何表格式数据源。它提供到大量 SQL 数据库的跨 DBMS 连通性。JDBC API 允许开发者利用 Java 平台的“一次编写,随处运行(Write Once, Run Anywhere)”的能力,以开发工业强度的、需要访问企业数据的跨平台应用程序。
SQLJ 是一种在 Java 中编写 SQL 访问的 ANSI 标准方法。SQLJ 比 JDBC 更加简明,因而更易于编写,并为更简单的调试提供了编译时模式验证和语法检查。在此方案中,SQL 语句嵌入在 Java 程序代码中,并且用 SQLJ 预编译程序将嵌入式 SQL 语句转换为 Java 方法调用。然而,由 SQLJ 预编译程序生成的代码不可能总是最佳的。
因为数据是跨数据库分区划分的,所以利用多个物理节点上多个处理器的能力来满足用户的请求成为可能。数据检索和更新请求都自动地分散到子请求中,并且在适用的数据库分区中并行地执行。对于发出 SQL 语句的应用程序,数据库被分区为多个数据库分区的事实是透明的。
应用程序交互作用发生在一个数据库分区(称为该应用程序的协调程序节点 - coordinator node)中。协调程序与应用程序运行在同一数据库分区上,或者如果是远程应用程序,则协调程序运行在应用程序连接到的数据库分区上。任何数据库分区都可以充当协调程序节点。关于分区数据库的更多信息,请参阅 Clustering for Scalability。
DB2 提供了一些特殊的编程功能以在分区环境中达到最佳的性能。缓冲插入(buffered inserts)便是一项技术的示例,该技术可以使决策支持系统中典型的插入密集型工作负载产生显著的性能增益。
本文中,我们将:
|
在分区环境中,DB2 UDB 可以利用表队列来缓冲正在插入的行。在类似 DSS 的插入密集型应用程序中,该功能可以使性能显著地提高。要使用缓冲插入,应用程序必须是已准备好的或被绑定并启用了 INSERT BUF 选项的。下面将讨论简单插入和缓冲插入的差别。
图 1中显示的是分区环境中简单数据库插入(无缓冲)的事件流。
如 图 2所示,如果该应用程序被绑定并启用了 INSERT BUF 选项,事件流会稍有不同。
请访问 DB2 UDB 7.2 Application Development Guide -- Chapter 18: Programming Considerations in a Partitioned Environment以获取会清空缓冲区的语句的完整列表。
缓冲插入带来更好的性能是由于:
要利用缓冲插入,您必须将应用程序绑定并启用 INSERT BUF 选项。在 JDBC 中,SQL 语句会在运行时动态地绑定。由于这种局限性,基于 JDBC 编写的代码无法使用缓冲插入。然而,在基于 SQLJ 的数据库代码中使用该功能是可能的。
|
让我们拿一个 Java 程序作为示例,该程序向定义如下的表中插入行:
create table testtable ( id integer not null, name char(20) not null, age smallint not null, address char(74) not null ) partitioning key (id); alter table testtable add primary key(id); |
名为 id 的那一列既是表的主键,也是表的分区键。
我们将实现此代码来使用两个类向这张表插入数据:
// TestTableRow.java
/**
* Represents a row of data in TestTable
*/
public class TestTableRow {
public int id;
public String name;
public short age;
public String address;
}
|
为简单起见,在 清单 1中我们保持成员变量为公用的。
清单 2中显示的类实现测试表的插入操作。
清单 2. 用于插入操作的 TestTableInserter.sqlj
// TestTableInserter.sqlj
import java.sql.*;
/**
* Implements the method to insert a row into testtable
*/
public class TestTableInserter {
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ref.DefaultContext _defaultContext;
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ExecutionContext _executionContext ;
/**
* Sole Constructor. Sets the protected DefaultContext variable and initializes the
* ExecutionContext to be used for the insert statement.
* @param connCtx DefaultContext object used for SQLJ operations
*/
public TestTableInserter( sqlj.runtime.ref.DefaultContext connCtx) {
_defaultContext = connCtx;
// This instance of ExecutionContext will be used in the insert operation
_executionContext = new sqlj.runtime.ExecutionContext( );
}
/**
* Inserts a single row into the testtable.
* @param row TestTableRow object containing data to be inserted
* @throws SQLException when database error occurs
*/
public void insertRow(TestTableRow row) throws SQLException {
int id = row.id;
String name = row.name;
short age = row.age;
String address = row.address;
#sql [_defaultContext,_executionContext] { insert into testtable values
(:id, :name, :age , :address) };
}
}
|
在 TestTableInserter.sqlj 上运行 SQLJ 转换程序(sqllibinsqlj.exe)。这样就用 Java 源代码语句替换了 SQLJ 程序中的 SQL 语句,并生成一个序列化概要文件,该文件包含关于 SQLJ 源文件中 SQL 语句的信息。
附录 A中显示了由转换程序生成的代码。
在缺省情况下,转换程序除创建此 java 文件之外,还执行以下步骤:
对于 SQL 处理的概述,请参阅 John Campbell 关于此课题的 Meet the Experts主题。
现在,运行 db2profc 命令以在生成的概要文件上安装 DB2 SQLJ 定制器并在目标 DB2 数据库中创建 DB2 程序包。
db2profc -user=dbuser -password=dbpwd -url=jdbc:db2:dbname -prepoptions= "bindfile using TestTableInserter.bnd package using TTblIns insert buf" TestTableInserter_SJProfile0.ser |
注:insert buf 选项(它被指定为 db2profc 命令的预选项属性的一部分)使得此 insert 语句成为缓冲插入的候选。此选项的缺省值是 insert def,它禁用缓冲插入。
|
由转换程序生成的代码用以下 清单 3中所示的代码片段替换了嵌入式插入语句。
1 {
2 sqlj.runtime.ConnectionContext __sJT_connCtx = _defaultContext;
3 if (__sJT_connCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_CONN_CTX();
4 sqlj.runtime.ExecutionContext __sJT_execCtx = _executionContext;
5 if (__sJT_execCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_EXEC_CTX();
6 int __sJT_1 = id;
7 String __sJT_2 = name;
8 short __sJT_3 = age;
9 String __sJT_4 = address;
10 synchronized (__sJT_execCtx) {
11 sqlj.runtime.profile.RTStatement __sJT_stmt = __sJT_execCtx.registerStatement
12 (__sJT_connCtx, TestTableInserter_SJProfileKeys.getKey(0), 0);
13 try
14 {
15 __sJT_stmt.setInt(1, __sJT_1);
16 __sJT_stmt.setString(2, __sJT_2);
17 __sJT_stmt.setShort(3, __sJT_3);
18 __sJT_stmt.setString(4, __sJT_4);
19 __sJT_execCtx.executeUpdate();
20 }
21 finally
22 {
23 __sJT_execCtx.releaseStatement();
24 }
25 }
26 }
|
从 清单 3明显可知,生成的代码为每个对 insertRow 函数的调用创建(行号:11)并破坏(行号:23)RTStatement 类的实例。这样开销太大。如 附录 B 中的 SQLJStmtCachedTestTableInserter 类所示,我修改了代码来除去这些开销,步骤如下:
|
对以下方案分析使用缓冲插入的性能增益。
图 3中所示的部署配置用于执行不同的测试用例。
在全部测试用例方案中,客户机应用程序连接到 Node 1,即测试数据库的目录节点(Catalog Node)。另外,分析中使用的测试数据在两个节点之间几乎平等地分布。
涉及的不同机器的配置如 表 1所示。
| 表 1:机器配置 | ||||
| 系统 | OS | CPU | 内存 | 软件 |
| Node 1 - 目录 | Win NT 4.0 (SP5) | Pentium III 647 | 196 MB | IBM DB2 UDB EEE 7.2 |
| Node 2 | Win NT 4.0 (SP5) | Pentium III 647 MHz | 196 MB | IBM DB2 UDB EEE 7.2 |
| 客户机 | Win 2000 Professional | Pentium III 800 MHz | 128 MB | IBM DB2 UDB EEE 7.2 客户机 |
SQLJStmtCachedTestTableInserter(重复使用的 RTStatement)和 TestTableInserter 类用于分析行的长度对缓冲插入的影响。客户机应用程序用于向测试表中插入 10000 行。随着地址列的数据类型的变化,行的大小在 0.1 KB 到 4 KB 之间变化。在测试程序结尾的单个提交用于将提交对缓冲插入的影响减到最小。在启用和禁用缓冲插入两种情况下都分别进行测试。对于不同的行的长度,也测量了使用普通 JDBC 代码的插入性能。结果如 图 4所示。
结果显示了使用缓冲插入的性能优势。对于长度较小的行,用缓冲插入每秒插入行的数量较之简单插入高出好多倍。随着行的长度的增加,由缓冲插入产生的性能增益迅速减小。这是因为,缓冲区在其清空之前所能保留的行数随着行的大小的增加变得更小。
如此前所讨论的那样,commit 语句(及其它一些语句)会导致缓冲插入中使用的缓冲区清空。因此随着提交频率的增加,由缓冲产生的性能增益减小。 图 5 比较了当使用以下代码时提交频率的影响:
附录 D中的清单显示了用于测试两个 SQLJ 变体的测试程序。
结果显示,随着提交频率的增加和行的大小的增大,使用缓冲插入的性能优势减小。
两个测试都在双节点的配置中进行。请注意,随着我们增加数据库节点的数量,性能会几乎呈线形提高。
在设计使用缓冲插入的应用程序时,必须对此功能的以下局限性给予应有的考虑。
在分区环境中,使用缓冲插入可能达到非常高的插入性能。要使用 Java 来利用此功能,代码必须基于 SQLJ。它对于向同一表执行插入密集型操作的应用程序很有用。对于行的长度很小的表,性能增益是很显著的。
|
| 描述 | 文件类型 | 文件大小 | 下载方法 |
| bufferedInsertSource.zip | zip | 10 KB | HTTP 下载 |
|
|
作者想要感谢 Infosys 的 Srinivas Thonse,感谢他对本文初稿细致的技术检查和他的鼓励;感谢 IBM 的 Cass Squire 和 Glen Sheffield,他们提出了许多深思熟虑的评论以及为发表此文章提供了指导;还要感谢 Kathy Zeidenstein 细致的编辑检查。
|
下面列出的是由 sqlj 转换程序生成的代码:
/*@lineinfo:filename=TestTableInserter*//*@lineinfo:user-code*//*@lineinfo:1^1*/// TestTableInserter.sqlj
import java.sql.*;
/**
* Implements the method to insert a row into testtable
*/
public class TestTableInserter {
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ref.DefaultContext _defaultContext;
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ExecutionContext _executionContext ;
/**
* Sole Constructor. Sets the protected DefaultContext variable and initializes the
* ExecutionContext to be used for the insert statement.
* @param connCtx DefaultContext object used for SQLJ operations
*/
public TestTableInserter( sqlj.runtime.ref.DefaultContext connCtx) {
_defaultContext = connCtx;
// This instance of ExecutionContext will be used in the insert operation
_executionContext = new sqlj.runtime.ExecutionContext( );
}
/**
* Inserts a single row into the testtable.
* @param row TestTableRow object containing data to be inserted
* @throws SQLException when database error occurs
*/
public void insertRow(TestTableRow row) throws SQLException {
int id = row.id;
String name = row.name;
short age = row.age;
String address = row.address;
/*@lineinfo:generated-code*//*@lineinfo:40^4*/
// ************************************************************
// #sql [_defaultContext,_executionContext] { insert into testtable values (:id, :name, :age , :address) };
// ************************************************************
{
sqlj.runtime.ConnectionContext __sJT_connCtx = _defaultContext;
if (__sJT_connCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_CONN_CTX();
sqlj.runtime.ExecutionContext __sJT_execCtx = _executionContext;
if (__sJT_execCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_EXEC_CTX();
int __sJT_1 = id;
String __sJT_2 = name;
short __sJT_3 = age;
String __sJT_4 = address;
synchronized (__sJT_execCtx) {
sqlj.runtime.profile.RTStatement __sJT_stmt = __sJT_execCtx.registerStatement
(__sJT_connCtx, TestTableInserter_SJProfileKeys.getKey(0), 0);
try
{
__sJT_stmt.setInt(1, __sJT_1);
__sJT_stmt.setString(2, __sJT_2);
__sJT_stmt.setShort(3, __sJT_3);
__sJT_stmt.setString(4, __sJT_4);
__sJT_execCtx.executeUpdate();
}
finally
{
__sJT_execCtx.releaseStatement();
}
}
}
// ************************************************************
/*@lineinfo:user-code*//*@lineinfo:40^106*/
}
}/*@lineinfo:generated-code*/class TestTableInserter_SJProfileKeys
{
private static TestTableInserter_SJProfileKeys inst = null;
public static java.lang.Object getKey(int keyNum)
throws java.sql.SQLException
{
if (inst == null)
{
inst = new TestTableInserter_SJProfileKeys();
}
return inst.keys[keyNum];
}
private final sqlj.runtime.profile.Loader loader = sqlj.runtime.RuntimeContext.getRuntime().getLoaderForClass(getClass());
private java.lang.Object[] keys;
private TestTableInserter_SJProfileKeys()
throws java.sql.SQLException
{
keys = new java.lang.Object[1];
keys[0] = sqlj.runtime.ref.DefaultContext.getProfileKey(loader, "TestTableInserter_SJProfile0");
}
}
|
|
下面列出的是性能分析中使用的修改过的代码:
/*@lineinfo:filename=SQLJStmtCachedTestTableInserter*//*@lineinfo:user-code*//*@lineinfo:1^1*/// TestTableInserter.sqlj
import java.sql.*;
/**
* Implements the method to insert a row into testtable
*/
public class SQLJStmtCachedTestTableInserter {
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ref.DefaultContext _defaultContext;
/**
* Stores the DefaultContext object used in the insert statement
*/
protected sqlj.runtime.ExecutionContext _executionContext ;
/**
* Stores the registered insert statement
*/
protected sqlj.runtime.profile.RTStatement _insertStatement ;
/**
* Releases the RTStatement object during garbage collection
* @throws SQLException if releaseStatement call fails
*/
protected void finalize() throws SQLException {
_executionContext.releaseStatement();
}
/**
* Sole Constructor. Sets the protected DefaultContext variable and initializes the
* ExecutionContext to be used for the insert statement.
* @param connCtx DefaultContext object used for SQLJ operations
* @throws SQLException if registerStatement call fails
*/
public SQLJStmtCachedTestTableInserter( sqlj.runtime.ref.DefaultContext connCtx) throws SQLException {
_defaultContext = connCtx;
// This instance of ExecutionContext will be used in the insert operation
_executionContext = new sqlj.runtime.ExecutionContext( );
_insertStatement = _executionContext.registerStatement(_defaultContext, TestTableInserter_SJProfileKeys.getKey(0), 0);
}
/**
* Inserts a single row into the testtable.
* @param row TestTableRow object containing data to be inserted
* @throws SQLException when database error occurs
*/
public void insertRow(TestTableRow row) throws SQLException {
int id = row.id;
String name = row.name;
short age = row.age;
String address = row.address;
/*@lineinfo:generated-code*//*@lineinfo:40^4*/
// ************************************************************
// #sql [_defaultContext,_executionContext] { insert into testtable values (:id, :name, :age , :address) };
// ************************************************************
{
sqlj.runtime.ConnectionContext __sJT_connCtx = _defaultContext;
if (__sJT_connCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_CONN_CTX();
sqlj.runtime.ExecutionContext __sJT_execCtx = _executionContext;
if (__sJT_execCtx == null) sqlj.runtime.error.RuntimeRefErrors.raise_NULL_EXEC_CTX();
int __sJT_1 = id;
String __sJT_2 = name;
short __sJT_3 = age;
String __sJT_4 = address;
synchronized (__sJT_execCtx) {
_insertStatement.setInt(1, __sJT_1);
_insertStatement.setString(2, __sJT_2);
_insertStatement.setShort(3, __sJT_3);
_insertStatement.setString(4, __sJT_4);
__sJT_execCtx.executeUpdate();
}
}
// ************************************************************
/*@lineinfo:user-code*//*@lineinfo:40^106*/
}
}/*@lineinfo:generated-code*/class TestTableInserter_SJProfileKeys
{
private static TestTableInserter_SJProfileKeys inst = null;
public static java.lang.Object getKey(int keyNum)
throws java.sql.SQLException
{
if (inst == null)
{
inst = new TestTableInserter_SJProfileKeys();
}
return inst.keys[keyNum];
}
private final sqlj.runtime.profile.Loader loader = sqlj.runtime.RuntimeContext.getRuntime().getLoaderForClass(getClass());
private java.lang.Object[] keys;
private TestTableInserter_SJProfileKeys()
throws java.sql.SQLException
{
keys = new java.lang.Object[1];
keys[0] = sqlj.runtime.ref.DefaultContext.getProfileKey(loader, "TestTableInserter_SJProfile0");
}
}
|
|
import java.sql.*;
import java.util.*;
class JDBCTableInserterTester
{
static
{
try
{
Class.forName("COM.ibm.db2.jdbc.app.DB2Driver").newInstance();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static void main(String args[]) throws Exception
{
String url = "jdbc:db2:testdb";
Properties prop = new Properties();
prop.put("user", "user");
prop.put("password", "pwd");
Connection con = null;
int commitFrequency ;
if (args.length ==0 )
commitFrequency = 10000;
else
commitFrequency = Integer.parseInt(args[0]);
try
{
// connect with user id/password
con = DriverManager.getConnection(url, prop);
con.setAutoCommit(false);
}
catch (SQLException e)
{
System.out.println("Error: could not open connection to database");
System.err.println(e) ;
System.exit(1);
}
System.out.println("Starting to insert : " + new java.util.Date() ) ;
PreparedStatement inserter = con.prepareStatement("insert into testtable values(?,?,?,?)");;
String name = "myname";
short age = 27;
char[] arr = new char[74];
Arrays.fill(arr, 'a');
String address = new String(arr);
for ( int id = 0; id < 10000; id++ ) {
inserter.setInt(1, id);
inserter.setString(2,name);
inserter.setShort(3,age);
inserter.setString(4, address);
inserter.executeUpdate();
if ((id + 1) % commitFrequency == 0) con.commit();
}
con.commit();
System.out.println("Finished inserting : " + new java.util.Date() ) ;
}
}
|
|
import java.sql.*;
import java.util.*;
import sqlj.runtime.*;
import sqlj.runtime.ref.*;
class SQLJTableInserterTester
{
static
{
try
{
Class.forName("COM.ibm.db2.jdbc.app.DB2Driver").newInstance();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static void main(String args[]) throws Exception
{
String url = "jdbc:db2:testdb";
Properties prop = new Properties();
prop.put("user", "user");
prop.put("password", "pwd");
int commitFrequency ;
if (args.length ==0 )
commitFrequency = 10000;
else
commitFrequency = Integer.parseInt(args[0]);
Connection con = null;
DefaultContext ctx = null;
try
{
// connect with default id/password
con = DriverManager.getConnection(url, prop);
con.setAutoCommit(false);
ctx = new DefaultContext(con);
}
catch (SQLException e)
{
System.out.println("Error: could not get a default context");
System.err.println(e) ;
System.exit(1);
}
System.out.println("Starting to insert : " + new java.util.Date() ) ;
TestTableRow row = new TestTableRow();
TestTableInserter inserter = new TestTableInserter(ctx);
row.name = "myname";
row.age = 27;
char[] arr = new char[74];
Arrays.fill(arr, 'a');
row.address = new String(arr);
for ( int id = 0; id < 10000; id++ ) {
row.id = id;
inserter.insertRow(row);
if ( (id + 1) % commitFrequency == 0 ) con.commit();
}
con.commit();
System.out.println("Finished inserting : " + new java.util.Date() ) ;
}
}
|
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/374079/viewspace-130350/ ,如需转载,请注明出处,否则将追究法律责任。