Eine einfache Anleitung zum Verbindungs-Pooling in Java

Ein einfacher Leitfaden zum Verbindungspooling in Java

1. Überblick

Das Verbindungspooling ist ein bekanntes Datenzugriffsmuster, dessen Hauptzweck darin besteht, den mit dem Durchführen von Datenbankverbindungen und Lese- / Schreib-Datenbankoperationen verbundenen Overhead zu verringern.

Kurz gesagt,a connection pool is, at the most basic level, a database connection cache implementation, die für bestimmte Anforderungen konfiguriert werden können.

In diesem Tutorial werden wir einige beliebte Frameworks für das Verbindungspooling kurz zusammenfassen und lernen, wie Sie unseren eigenen Verbindungspool von Grund auf neu implementieren.

2. Warum Verbindungspooling?

Die Frage ist natürlich rhetorisch.

Wenn wir die Abfolge der Schritte analysieren, die in einem typischen Lebenszyklus einer Datenbankverbindung enthalten sind, werden wir verstehen, warum:

  1. Herstellen einer Verbindung zur Datenbank mithilfe des Datenbanktreibers

  2. Öffnen einesTCP socket zum Lesen / Schreiben von Daten

  3. Lesen / Schreiben von Daten über den Socket

  4. Verbindung trennen

  5. Steckdose schließen

Es wird deutlich, dassdatabase connections are fairly expensive operations und als solche in jedem möglichen Anwendungsfall auf ein Minimum reduziert werden sollten (in Randfällen nur vermieden).

Hier kommen Implementierungen von Verbindungspools ins Spiel.

Durch die einfache Implementierung eines Datenbankverbindungscontainers, mit dem wir eine Reihe vorhandener Verbindungen wiederverwenden können, können wir die Kosten für die Durchführung einer großen Anzahl teurer Datenbankreisen wirksam einsparen und somit die Gesamtleistung unserer datenbankgesteuerten Anwendungen steigern.

3. JDBC-Verbindungspooling-Frameworks

Aus pragmatischer Sicht ist die Implementierung eines Verbindungspools von Grund auf sinnlos, wenn man bedenkt, wie viele unternehmensfähige Verbindungspooling-Frameworks zur Verfügung stehen.

Von einer didaktischen, die das Ziel dieses Artikels ist, ist es nicht.

Bevor wir jedoch lernen, wie ein grundlegender Verbindungspool implementiert wird, wollen wir zunächst einige beliebte Frameworks für das Verbindungspooling vorstellen.

3.1. Apache Commons DBCP

Beginnen wir diese kurze Zusammenfassung mitApache Commons DBCP Component, einem JDBC-Framework mit umfassendem Funktionspooling:

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(){ }
}

In diesem Fall haben wir eine Wrapper-Klasse mit einem statischen Block verwendet, um die Eigenschaften von DBCP einfach zu konfigurieren.

So erhalten Sie eine Poolverbindung mit der KlasseDBCPDataSource:

Connection con = DBCPDataSource.getConnection();

3.2. HikariCP

Schauen wir uns im FolgendenHikariCP an, ein blitzschnelles JDBC-Verbindungspooling-Framework, das vonBrett Wooldridge erstellt wurde. (Ausführliche Informationen zum Konfigurieren und Optimieren von HikariCP finden Sie unterthis 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(){}
}

In ähnlicher Weise erfahren Sie hier, wie Sie eine Poolverbindung mit der KlasseHikariCPDataSourceherstellen:

Connection con = HikariCPDataSource.getConnection();

3.3. C3PO

Der letzte Teil dieser Überprüfung istC3PO, ein leistungsstarkes JDBC4-Framework für das Verbinden von Verbindungen und Anweisungen, das von Steve Waldman entwickelt wurde:

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(){}
}

Wie erwartet ähnelt das Abrufen einer Poolverbindung mit der KlasseC3poDataSourceden vorherigen Beispielen:

Connection con = C3poDataSource.getConnection();

4. Eine einfache Implementierung

Um die zugrunde liegende Logik des Verbindungspoolings besser zu verstehen, erstellen wir eine einfache Implementierung.

Beginnen wir mit einem lose gekoppelten Design, das auf nur einer einzigen Schnittstelle basiert:

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

DieConnectionPool-Schnittstelle definiert die öffentliche API eines Basisverbindungspools.

Erstellen wir nun eine Implementierung, die einige grundlegende Funktionen bietet, darunter das Abrufen und Freigeben einer Poolverbindung:

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
}

Die KlasseBasicConnectionPoolist zwar ziemlich naiv, bietet jedoch die minimale Funktionalität, die wir von einer typischen Implementierung von Verbindungspools erwarten.

Kurz gesagt, die Klasse initialisiert einen Verbindungspool basierend auf einemArrayList, in dem 10 Verbindungen gespeichert sind, die leicht wiederverwendet werden können.

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

Da es viel besser ist, die Erstellung von Verbindungsdatenbanken unabhängig zu halten, haben wir die erstere im Rahmen der statischen Factory-Methode voncreate()verwendet.

In diesem Fall haben wir die Methode innerhalb vonBasicConnectionPool platziert, da dies die einzige Implementierung der Schnittstelle ist.

In einem komplexeren Design mit mehrerenConnectionPool-Implementierungen ist es vorzuziehen, es in der Schnittstelle zu platzieren, um ein flexibleres Design und einen höheren Grad an Kohäsion zu erhalten.

Der wichtigste Punkt, der hier hervorgehoben werden muss, ist, dass nach dem Erstellen des Poolsconnections are fetched from the pool, so there’s no need to create new ones.

Weiterhinwhen a connection is released, it’s actually returned back to the pool, so other clients can reuse it.

Es gibt keine weitere Interaktion mit der zugrunde liegenden Datenbank, z. B. einen expliziten Aufruf derConnection’s close()-Methode.

5. Verwenden derBasicConnectionPool-Klasse

Wie erwartet ist die Verwendung unsererBasicConnectionPool-Klasse unkompliziert.

Erstellen wir einen einfachen Komponententest und erhalten eine gepoolte in-memoryH2-Verbindung:

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

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

6. Weitere Verbesserungen und Refactoring

Natürlich gibt es viel Raum, um die aktuelle Funktionalität unserer Implementierung des Verbindungspools zu optimieren / zu erweitern.

Zum Beispiel könnten wir diegetConnection()-Methode umgestalten und Unterstützung für die maximale Poolgröße hinzufügen. Wenn alle verfügbaren Verbindungen hergestellt wurden und die aktuelle Poolgröße unter dem konfigurierten Maximum liegt, erstellt die Methode eine neue Verbindung:

@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;
}

Beachten Sie, dass die Methode jetztSQLException auslöst, was bedeutet, dass wir auch die Schnittstellensignatur aktualisieren müssen.

Oder wir könnten eine Methode hinzufügen, um unsere Verbindungspoolinstanz ordnungsgemäß herunterzufahren:

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

In produktionsbereiten Implementierungen sollte ein Verbindungspool eine Reihe zusätzlicher Funktionen bereitstellen, z. B. die Möglichkeit zum Verfolgen der derzeit verwendeten Verbindungen, die Unterstützung für das Pooling vorbereiteter Anweisungen usw.

Da wir die Dinge einfach halten, werden wir die Implementierung dieser zusätzlichen Funktionen unterlassen und die Implementierung aus Gründen der Übersichtlichkeit nicht threadsicher halten.

7. Fazit

In diesem Artikel haben wir uns eingehend mit dem Thema Verbindungspooling befasst und erfahren, wie unsere eigene Verbindungspooling-Implementierung umgesetzt wird.

Natürlich müssen wir nicht jedes Mal von vorne anfangen, wenn wir unseren Anwendungen eine voll funktionsfähige Verbindungspooling-Schicht hinzufügen möchten.

Aus diesem Grund haben wir zunächst eine einfache Zusammenfassung erstellt, in der einige der beliebtesten Frameworks für Verbindungspools aufgeführt sind, damit wir eine klare Vorstellung davon haben, wie wir mit ihnen arbeiten können, und dasjenige auswählen können, das unseren Anforderungen am besten entspricht.

Wie üblich sind alle in diesem Artikel gezeigten Codebeispieleover on GitHub verfügbar.