1. Общ преглед
Пулът на връзките е добре познат модел за достъп до данни, чиято основна цел е да намали режийните разходи, свързани с извършването на връзки с база данни и операции за четене / запис на база данни.
Накратко, пулът от връзки е, на най-основното ниво, изпълнение на кеш за връзка с база данни , което може да бъде конфигурирано да отговаря на специфични изисквания.
В този урок ще направим кратък преглед на няколко популярни рамки за обединяване на връзки и ще научим как да прилагаме от нулата собствения си пул от връзки.
2. Защо обединяване на връзки?
Въпросът е риторичен, разбира се.
Ако анализираме последователността от стъпки, включени в типичен жизнен цикъл на свързване с база данни, ще разберем защо:
- Отваряне на връзка с базата данни с помощта на драйвера на базата данни
- Отваряне на TCP сокет за четене / запис на данни
- Четене / запис на данни през сокета
- Затваряне на връзката
- Затваряне на гнездото
Става очевидно, че връзките с бази данни са доста скъпи операции и като такива трябва да бъдат сведени до минимум във всеки възможен случай на употреба (в крайни случаи, просто избягвани).
Ето къде влизат в изпълнение изпълненията за обединяване на връзки.
Чрез простото внедряване на контейнер за връзка с база данни, което ни позволява да използваме повторно редица съществуващи връзки, можем ефективно да спестим разходите за извършване на огромен брой скъпи пътувания с база данни, като по този начин повишим общата производителност на нашите приложения, управлявани от база данни.
3. Рамки за обединяване на връзки JDBC
От прагматична гледна точка, внедряването на пул от връзки от самото начало е просто безсмислено, като се има предвид броят на наличните там рамки за обединяване на връзки, „готови за предприятие“.
От дидактическа, която е целта на тази статия, не е така.
Въпреки това, преди да научим как да реализираме основен пул от връзки, нека първо да покажем няколко популярни рамки за обединяване на връзки.
3.1. Apache Commons DBCP
Нека започнем този бърз обзор с Apache Commons DBCP Component, пълнофункционална рамка за обединяване на JDBC:
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. HikariCP
Продължавайки, нека разгледаме HikariCP, светкавично бърза рамка за обединяване на връзки JDBC, създадена от Brett Wooldridge (за пълните подробности за това как да конфигурирате и извлечете максимума от HikariCP, моля, проверете тази статия):
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
Последно в този преглед е C3PO, мощна JDBC4 връзка и рамка за обединяване на изявления, разработена от Стив Уолдман:
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(); }
The ConnectionPool interface defines the public API of a basic connection pool.
Now, let's create an implementation, which provides some basic functionality, including getting and releasing a pooled connection:
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 }
While pretty naive, the BasicConnectionPool class provides the minimal functionality that we'd expect from a typical connection pooling implementation.
In a nutshell, the class initializes a connection pool based on an ArrayList that stores 10 connections, which can be easily reused.
It's possible to create JDBC connections with the DriverManager class and with Datasource implementations.
As it's much better to keep the creation of connections database agnostic, we've used the former, within the create() static factory method.
In this case, we've placed the method within the BasicConnectionPool, because this is the only implementation of the interface.
In a more complex design, with multiple ConnectionPool implementations, it'd be preferable to place it in the interface, therefore getting a more flexible design and a greater level of cohesion.
The most relevant point to stress here is that once the pool is created, connections are fetched from the pool, so there's no need to create new ones.
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.
Тъй като ще улесним нещата, ще пропуснем как да внедрим тези допълнителни функции и ще запазим внедряването, което не е безопасно за нишки, за по-голяма яснота.
7. Заключение
В тази статия разгледахме задълбочено какво е обединяване на връзки и се научихме как да разгърнем собствената си реализация на обединяване на връзки.
Разбира се, не е нужно да започваме от нулата всеки път, когато искаме да добавим пълнофункционален слой за обединяване на връзки към нашите приложения.
Ето защо първо направихме обикновен обзор, показващ някои от най-популярните рамки на пула за свързване, за да можем да имаме ясна представа как да работим с тях и да вземем този, който най-добре отговаря на нашите изисквания.
Както обикновено, всички примерни кодове, показани в тази статия, са достъпни в GitHub.