Eine einfache Anleitung zum Verbindungspooling in Java

1. Übersicht

Verbindungspooling ist ein bekanntes Datenzugriffsmuster, dessen Hauptzweck darin besteht, den Aufwand für die Durchführung von Datenbankverbindungen und das Lesen / Schreiben von Datenbankoperationen zu verringern.

Kurz gesagt, ein Verbindungspool ist auf der einfachsten Ebene eine Datenbankverbindungs-Cache-Implementierung , die für bestimmte Anforderungen konfiguriert werden kann.

In diesem Tutorial werden wir einige beliebte Frameworks für das Verbindungspooling kurz zusammenfassen und lernen, wie wir 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. Öffnen einer Verbindung zur Datenbank mit dem Datenbanktreiber
  2. Öffnen eines TCP-Sockets zum Lesen / Schreiben von Daten
  3. Lesen / Schreiben von Daten über den Socket
  4. Verbindung schließen
  5. Steckdose schließen

Es wird deutlich, dass Datenbankverbindungen ziemlich teure Vorgänge sind und als solche in jedem möglichen Anwendungsfall auf ein Minimum reduziert werden sollten (in Randfällen nur vermieden).

Hier kommen Verbindungspooling-Implementierungen ins Spiel.

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

3. JDBC Connection Pooling Frameworks

Aus pragmatischer Sicht ist die Implementierung eines Verbindungspools von Grund auf angesichts der Anzahl der verfügbaren "Enterprise-Ready" -Verbindungspooling-Frameworks einfach sinnlos.

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 mit Apache 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 DBCPDataSource- Klasse:

Connection con = DBCPDataSource.getConnection();

3.2. HikariCP

Schauen wir uns zunächst HikariCP an, ein blitzschnelles JDBC-Framework für das Zusammenlegen von Verbindungen, das von Brett Wooldridge erstellt wurde (Einzelheiten zur Konfiguration und optimalen Nutzung von HikariCP finden Sie in diesem Artikel):

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 , wie Sie eine Poolverbindung mit der HikariCPDataSource- Klasse herstellen:

Connection con = HikariCPDataSource.getConnection();

3.3. C3PO

Der letzte Teil dieser Überprüfung ist C3PO, 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 Herstellen einer Poolverbindung mit der C3poDataSource- Klasse den 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(); }

Die ConnectionPool- Schnittstelle definiert die öffentliche API eines grundlegenden Verbindungspools.

Lassen Sie uns nun eine Implementierung erstellen, die einige grundlegende Funktionen bietet, einschließlich des Abrufens und Freigebens 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 BasicConnectionPool- Klasse ist zwar ziemlich naiv, bietet jedoch die minimale Funktionalität, die wir von einer typischen Implementierung des Verbindungspools erwarten würden.

Kurz gesagt, die Klasse initialisiert einen Verbindungspool basierend auf einer ArrayList , in der 10 Verbindungen gespeichert sind , die problemlos wiederverwendet werden können.

Es ist möglich, JDBC-Verbindungen mit der DriverManager- Klasse und mit Datenquellenimplementierungen zu erstellen .

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

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

In einem komplexeren Design mit mehreren ConnectionPool- Implementierungen ist es vorzuziehen, es in der Benutzeroberfläche zu platzieren, um ein flexibleres Design und ein höheres Maß an Kohäsion zu erhalten.

Der wichtigste Punkt, der hier hervorgehoben werden muss, ist, dass nach dem Erstellen des Pools Verbindungen aus dem Pool abgerufen werden, sodass keine neuen erstellt werden müssen .

Furthermore, when a connection is released, it's actually returned back to the pool, so other clients can reuse it.

There's no any further interaction with the underlying database, such as an explicit call to the Connection's close() method.

5. Using the BasicConnectionPool Class

As expected, using our BasicConnectionPool class is straightforward.

Let's create a simple unit test and get a pooled in-memory H2 connection:

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

6. Further Improvements and Refactoring

Of course, there's plenty of room to tweak/extend the current functionality of our connection pooling implementation.

For instance, we could refactor the getConnection() method, and add support for maximum pool size. If all available connections are taken, and the current pool size is less than the configured maximum, the method will create a new connection.

Also, we could additionally verify whether the connection obtained from the pool is still alive, before passing it to the client.

@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); if(!connection.isValid(MAX_TIMEOUT)){ connection = createConnection(url, user, password); } usedConnections.add(connection); return connection; } 

Note that the method now throws SQLException, meaning we'll have to update the interface signature as well.

Or, we could add a method to gracefully shut down our connection pool instance:

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

In production-ready implementations, a connection pool should provide a bunch of extra features, such as the ability for tracking the connections that are currently in use, support for prepared statement pooling, and so forth.

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

7. Fazit

In diesem Artikel haben wir uns eingehend mit dem Verbindungspooling befasst und gelernt, wie unsere eigene Implementierung des Verbindungspoolings 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 sollen, und diejenige auswählen können, die unseren Anforderungen am besten entspricht.

Wie üblich sind alle in diesem Artikel gezeigten Codebeispiele auf GitHub verfügbar.