Javaでのコネクションプーリングの簡単なガイド

Javaでの接続プーリングの簡単なガイド

1. 概要

接続プーリングはよく知られたデータアクセスパターンであり、その主な目的は、データベース接続の実行とデータベースの読み取り/書き込み操作に伴うオーバーヘッドを削減することです。

一言で言えば、a connection pool is, at the most basic level, a database connection cache implementationは、特定の要件に合うように構成できます。

このチュートリアルでは、いくつかの一般的な接続プールフレームワークを簡単にまとめ、独自の接続プールを最初から実装する方法を学習します。

2. なぜ接続プーリングなのか?

もちろん、質問は修辞的です。

一般的なデータベース接続のライフサイクルに含まれる一連の手順を分析すると、次の理由がわかります。

  1. データベースドライバーを使用してデータベースへの接続を開く

  2. データの読み取り/書き込み用にTCP socketを開く

  3. ソケットを介したデータの読み取り/書き込み

  4. 接続を閉じる

  5. ソケットを閉じる

database connections are fairly expensive operationsは、考えられるすべてのユースケースで最小限に抑える必要があることが明らかになります(エッジの場合は避けてください)。

ここで、接続プールの実装が役立ちます。

データベース接続コンテナーを実装するだけで、既存の多数の接続を再利用できるため、膨大な費用のかかるデータベーストリップを実行するコストを効果的に節約できるため、データベース駆動型アプリケーションの全体的なパフォーマンスが向上します。

3. JDBC接続プールフレームワーク

実用的な観点から、接続プールをゼロから実装することは、そこにある「エンタープライズ対応」接続プールフレームワークの数を考えると、無意味です。

この記事の目的である教訓的なものから、そうではありません。

それでも、基本的な接続プールを実装する方法を学ぶ前に、まずいくつかの一般的な接続プールフレームワークを紹介しましょう。

3.1. Apache Commons DBCP

この簡単なまとめを、フル機能の接続プールJDBCフレームワークであるApache Commons DBCP Componentから始めましょう。

public class DBCPDataSource {

    private static BasicDataSource ds = new BasicDataSource();

    static {
        ds.setUrl("jdbc:h2:mem:test");
        ds.setUsername("user");
        ds.setPassword("password");
        ds.setMinIdle(5);
        ds.setMaxIdle(10);
        ds.setMaxOpenPreparedStatements(100);
    }

    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }

    private DBCPDataSource(){ }
}

この場合、静的ブロックを持つラッパークラスを使用して、DBCPのプロパティを簡単に構成しました。

DBCPDataSourceクラスでプールされた接続を取得する方法は次のとおりです。

Connection con = DBCPDataSource.getConnection();

3.2. ひかりCP

次に、Brett Wooldridgeによって作成された超高速のJDBC接続プールフレームワークであるHikariCPを見てみましょう(HikariCPを構成して最大限に活用する方法の詳細については、this articleを確認してください)。 )::

public class HikariCPDataSource {

    private static HikariConfig config = new HikariConfig();
    private static HikariDataSource ds;

    static {
        config.setJdbcUrl("jdbc:h2:mem:test");
        config.setUsername("user");
        config.setPassword("password");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        ds = new HikariDataSource(config);
    }

    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }

    private HikariCPDataSource(){}
}

同様に、HikariCPDataSourceクラスでプールされた接続を取得する方法は次のとおりです。

Connection con = HikariCPDataSource.getConnection();

3.3. C3PO

このレビューの最後は、Steve Waldmanによって開発された強力なJDBC4接続およびステートメントプーリングフレームワークであるC3POです。

public class C3poDataSource {

    private static ComboPooledDataSource cpds = new ComboPooledDataSource();

    static {
        try {
            cpds.setDriverClass("org.h2.Driver");
            cpds.setJdbcUrl("jdbc:h2:mem:test");
            cpds.setUser("user");
            cpds.setPassword("password");
        } catch (PropertyVetoException e) {
            // handle the exception
        }
    }

    public static Connection getConnection() throws SQLException {
        return cpds.getConnection();
    }

    private C3poDataSource(){}
}

予想どおり、C3poDataSourceクラスでプールされた接続を取得することは、前の例と同様です。

Connection con = C3poDataSource.getConnection();

4. 簡単な実装

接続プーリングの基礎となるロジックをよりよく理解するために、簡単な実装を作成しましょう。

単一のインターフェースのみに基づいた、疎結合の設計から始めましょう。

public interface ConnectionPool {
    Connection getConnection();
    boolean releaseConnection(Connection connection);
    String getUrl();
    String getUser();
    String getPassword();
}

ConnectionPoolインターフェースは、基本的な接続プールのパブリックAPIを定義します。

それでは、プールされた接続の取得と解放など、いくつかの基本的な機能を提供する実装を作成しましょう。

public class BasicConnectionPool
  implements ConnectionPool {

    private String url;
    private String user;
    private String password;
    private List connectionPool;
    private List usedConnections = new ArrayList<>();
    private static int INITIAL_POOL_SIZE = 10;

    public static BasicConnectionPool create(
      String url, String user,
      String password) throws SQLException {

        List pool = new ArrayList<>(INITIAL_POOL_SIZE);
        for (int i = 0; i < INITIAL_POOL_SIZE; i++) {
            pool.add(createConnection(url, user, password));
        }
        return new BasicConnectionPool(url, user, password, pool);
    }

    // standard constructors

    @Override
    public Connection getConnection() {
        Connection connection = connectionPool
          .remove(connectionPool.size() - 1);
        usedConnections.add(connection);
        return connection;
    }

    @Override
    public boolean releaseConnection(Connection connection) {
        connectionPool.add(connection);
        return usedConnections.remove(connection);
    }

    private static Connection createConnection(
      String url, String user, String password)
      throws SQLException {
        return DriverManager.getConnection(url, user, password);
    }

    public int getSize() {
        return connectionPool.size() + usedConnections.size();
    }

    // standard getters
}

かなりナイーブですが、BasicConnectionPoolクラスは、一般的な接続プールの実装に期待される最小限の機能を提供します。

一言で言えば、クラスは、簡単に再利用できる10個の接続を格納するArrayListに基づいて接続プールを初期化します。

It’s possible to create JDBC connections with the DriverManager class and with Datasource implementations

接続データベースの作成を不可知論に保つ方がはるかに優れているため、create()静的ファクトリメソッド内で前者を使用しました。

この場合、これがインターフェースの唯一の実装であるため、メソッドをBasicConnectionPool内に配置しました。

複数のConnectionPoolが実装されている、より複雑な設計では、インターフェイスに配置することをお勧めします。これにより、より柔軟な設計とより高いレベルの結束が得られます。

ここで強調する最も重要な点は、プールが作成されると、connections are fetched from the pool, so there’s no need to create new onesになることです。

さらに、when a connection is released, it’s actually returned back to the pool, so other clients can reuse it

Connection’s close()メソッドの明示的な呼び出しなど、基盤となるデータベースとのそれ以上の相互作用はありません。

5. BasicConnectionPoolクラスの使用

予想どおり、BasicConnectionPoolクラスの使用は簡単です。

簡単な単体テストを作成して、プールされた in-memoryH2接続を取得しましょう。

@Test
public whenCalledgetConnection_thenCorrect() {
    ConnectionPool connectionPool = BasicConnectionPool
      .create("jdbc:h2:mem:test", "user", "password");

    assertTrue(connectionPool.getConnection().isValid(1));
}

6. さらなる改善とリファクタリング

もちろん、接続プールの実装の現在の機能を微調整/拡張する余地は十分にあります。

たとえば、getConnection()メソッドをリファクタリングし、最大プールサイズのサポートを追加できます。 使用可能なすべての接続が取得され、現在のプールサイズが設定された最大サイズよりも小さい場合、メソッドは新しい接続を作成します。

@Override
public Connection getConnection() throws SQLException {
    if (connectionPool.isEmpty()) {
        if (usedConnections.size() < MAX_POOL_SIZE) {
            connectionPool.add(createConnection(url, user, password));
        } else {
            throw new RuntimeException(
              "Maximum pool size reached, no available connections!");
        }
    }

    Connection connection = connectionPool
      .remove(connectionPool.size() - 1);
    usedConnections.add(connection);
    return connection;
}

このメソッドはSQLExceptionをスローするようになりました。つまり、インターフェースの署名も更新する必要があります。

または、接続プールインスタンスを正常にシャットダウンするメソッドを追加できます。

public void shutdown() throws SQLException {
    usedConnections.forEach(this::releaseConnection);
    for (Connection c : connectionPool) {
        c.close();
    }
    connectionPool.clear();
}

実稼働対応の実装では、接続プールは、現在使用中の接続を追跡する機能、準備されたステートメントプーリングのサポートなど、多数の追加機能を提供する必要があります。

物事を単純にするため、これらの追加機能を実装する方法を省略し、わかりやすくするために実装をスレッドセーフではない状態に保ちます。

7. 結論

この記事では、接続プーリングとは何かを詳しく調べ、独自の接続プーリング実装を展開する方法を学びました。

もちろん、フル機能の接続プールレイヤーをアプリケーションに追加するたびに、最初から始める必要はありません。

そのため、最初に最も人気のある接続プールフレームワークのいくつかを示す簡単なまとめを作成しました。これにより、それらを操作する方法を明確に把握し、要件に最適なフレームワークを選択できます。

いつものように、この記事に示されているすべてのコードサンプルは利用可能なover on GitHubです。