読者です 読者をやめる 読者になる 読者になる

Murayama blog.

プログラミングと、その次の話

Commons DBCPを使ってみる

Commons DBCPについて調べてみました。
ちょっと前置きからスタートします。

Connectionオブジェクトの生成について

Javaは、DBとの接続をjava.sql.Connection型のオブジェクトとして取り扱います。
JDBCドライバを使った一般的なプログラムは以下のようになります。

	Class.forName("oracle.jdbc.driver.OracleDriver");
	Connection con = DriverManager.getConnection(
	"jdbc:oracle:thin:@localhost:1521/ORCL", "scott", "tiger");
Connectionオブジェクトの生成にかかる負荷

上記のプログラムの場合、

	Connection con = DriverManager.getConnection(
	"jdbc:oracle:thin:@localhost:1521/ORCL", "scott", "tiger");

の部分でConnectionオブジェクトを生成していますが、
これは一般的なオブジェクトの生成(例えばStringオブジェクトなど)と比べて、
非常に時間のかかる作業になります。
仮に上記のコードの実行に0.1秒かかる場合、
1回だけ実行するのであれば、それほど意識しなくてもよいかもしれませんが、
1000回繰り返し実行するとなると、100秒時間がかかることになります。

DBとの物理的なコネクション

DBとの物理的なコネクション(接続)について調べてみます。

	Connection con = DriverManager.getConnection(
	"jdbc:oracle:thin:@localhost:1521/ORCL", "scott", "tiger");

上記のコードを実行すると、
DBとの間に物理的なコネクション(接続)が生まれます。
Oracleで接続数(セッション数)を調べるには以下のようにコマンドを実行します。

select machine, program, count(*) from v$session where username != 'SYS' group by machine, program;

実行結果(セッション数)

MACHINE              PROGRAM       COUNT(*)
-------------------- ------------- --------
e9583f70                                  1    <-- これがJDBCの接続
WORKGROUP1\E9583F70  sqlplus.exe          1    <-- こっちはsqlplus

上記の実行結果から、プログラムからDBへ接続している様子が確認できます。

コネクションのクローズ

Javaでは、Connectionオブジェクトが不要になった場合、
プログラマーがcloseメソッドを呼び出すことで、をクローズするのが一般的です。

	con.close();

上記のプログラムを実行すると、コネクションがクローズされます。


と、ここがちょっとしたポイントです。
「コネクションのクローズ」とは一体どういうことでしょうか。

	con.close();

メソッドを実行すると、物理的なコネクション(OracleのV$SESSIONで確認したもの)は切断されるのでしょうか。
con.close()を実行すると、
物理的な接続が切れる、つまり、V$SESSIONから見えなくなる、と思うかもしれませんが、
厳密な意味では、これは正しくありません。
正確には、Connectionの実体(実装オブジェクト)に依存します。


ちなみに、ここまでのプログラム、

	Class.forName("oracle.jdbc.driver.OracleDriver");
	Connection con = DriverManager.getConnection(
	"jdbc:oracle:thin:@localhost:1521/ORCL", "scott", "tiger");
	
	// 検索とか更新とか
	
	con.close();

の場合は、closeメソッドの呼び出しにより、物理的なコネクションも切断されます。
一方で、これから紹介するDBCPでは、closeメソッドを呼び出したとしても、
必ず物理的なコネクションが切断される、というわけではなくなります。

コネクションプールとは

JavaのConnectionオブジェクトの生成は負荷のかかる作業です。
そのため、何度も繰り返し不必要にConnectionオブジェクトを生成することは好ましくありません。
とくに、Webアプリケーションを開発する場合は、ユーザからのリクエストに応じて、DBへ接続する必要があります。
ユーザからのリクエストごとにConnectionオブジェクトの生成を繰り返すことは好ましくありません。
そこで、一般的なAPサーバでは起動時にDBとの物理的な接続を予め複数用意しておき、
それをユーザのリクエストに対して使い回すようにすることで、上記の問題を解決します。
DBとの物理的な接続を予め複数用意することをコネクションプール(コネクションプーリング)と呼びます。


コネクションプールの仕組みは、APサーバによって提供されるものというわけではないので、
スタンドアロンJavaアプリケーションでも利用(あるいは実装)することができます。
コネクションプールを独自に実装することもできますが、
Commons DBCPを使うこともできます。*1

Commons DBCPとは

Jakartaプロジェクトで開発されているコネクションプーリングのライブラリです。
実装サンプル(呼び出しもと)は以下のようになります。

	Properties properties = new Properties();
	InputStream is = ClassLoader.getSystemResourceAsStream("dbcp.properties");
	properties.load(is);
	DataSource ds = BasicDataSourceFactory.createDataSource(properties);
	Connection con = ds.getConnection();


用意したdbcp.propertiesファイルは以下のとおり。

driverClassName=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@localhost:1521/ORCL
username=scott
password=tiger
initialSize=30
maxActive=100
maxIdle=30
maxWait=5000
validationQuery=select count(*) from dual

上記の設定だと、

  • initialSize・・・コネクションプールの初期コネクションの数は30件である。
  • maxActive・・・最大で100件までコネクションを作ることができる。
  • maxIdle・・・アイドル状態(未使用状態)のコネクションは30件保持される。
  • アイドル状態のコネクションが30件を超えている場合は、余計な件数分の物理コネクションが切断される。
  • maxWait・・・コネクション取得の待ち時間は5000msである。
  • validationQuery・・・コネクションが利用可能か検証するためのSQL


また、コネクションをクローズすると、

	con.close();

設定ファイルの閾値により、
コネクションプールに戻るか、物理的に切断されるかが決まります。

コネクションプールの効果測定

以下のプログラムで、コネクションプールの効果を測定してみました。

package sample;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class Main {
	
	private static final int CONNECTION_COUNT = 30;
	
	public static void main(String[] args) throws SQLException {

		System.out.println("start");
		
		ConnectionFactory factory = ConnectionFactory.getInstance();
		
		List<Connection> connections = createConnections(factory);
		releaseConnections(connections);		

		List<Connection> connections2 = createConnections(factory);
		releaseConnections(connections2);	

		List<Connection> connections3 = createConnections(factory);
		releaseConnections(connections3);		

		System.out.println("end");
	}

	private static List<Connection> createConnections(ConnectionFactory factory) throws SQLException {
		List<Connection> conList = new ArrayList<Connection>();
		long start = System.currentTimeMillis();
		for(int i = 0; i < CONNECTION_COUNT; i++){
			Connection con = factory.getConnection();
			// なんらかの処理		
//			PreparedStatement ps = con.prepareCall("select * from emp where empno = ?");
//			ps.setInt(1, 7369);
//			ResultSet rs = ps.executeQuery();
//			while(rs.next()){
//				System.out.println(rs.getString("ename"));
//			}
//			ps.close();
			conList.add(con);
		}
		long end = System.currentTimeMillis();
		System.out.println("createConnections 所要時間:" + (end - start));
		return conList;
	}

	private static void releaseConnections(List<Connection> conList)
			throws SQLException {
		long start = System.currentTimeMillis();

		for(int i = 0; i < CONNECTION_COUNT; i++){
			Connection con = conList.get(i);
			con.close();
		}
		conList.clear();
		long end = System.currentTimeMillis();
		System.out.println("releaseConnections 所要時間:" + (end - start));
	}

}
package sample;

import java.sql.Connection;

public abstract class ConnectionFactory {

	protected ConnectionFactory(){}
	
	public static ConnectionFactory getInstance(){
		return new DBCPConnectionFactory();
	}
	
	public abstract Connection getConnection();
	
}
package sample;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;

public class DBCPConnectionFactory extends ConnectionFactory {

	private DataSource ds;
	
	protected DBCPConnectionFactory(){
		Properties properties = new Properties();
		try {
			InputStream is = ClassLoader.getSystemResourceAsStream("dbcp.properties");
			properties.load(is);
			this.ds = BasicDataSourceFactory.createDataSource(properties);
		} catch (IOException e) {
			throw new RuntimeException(e);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	@Override
	public Connection getConnection() {
		try {
			return ds.getConnection();
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}
}

dbcp.propertiesは先のものと同じです。
実行結果は以下のとおり。

start
createConnections 所要時間:1437
releaseConnections 所要時間:0
createConnections 所要時間:31
releaseConnections 所要時間:0
createConnections 所要時間:16
releaseConnections 所要時間:0
end

createConnectionsメソッドの2回目以降の所要時間が大きく短縮されました。

*1:Tomcatのコネクションプーリングには、DBCPが利用されているそうです