Mục lục
Connection pooling là một data access pattern với mục đích chính là giảm thiểu sử dụng tài nguyên của ứng dụng cho việc thực hiện các kết nối và các hoạt động đọc/ghi đến cơ sở dữ liệu.
Connection pooling cho phép tạo và duy trì một tập các kết nối (connection) dùng chung nhằm tăng hiệu suất cho các ứng dụng bằng cách sử dụng lại các kết nối khi có yêu cầu xử lý dữ liệu đến database thay vì việc tạo kết nối mới. Vùng chứa các connection này gọi là Pool.
Tóm lại nó giống như là một database connection cache lưu trữ các kết nối đến đã được tạo đến database, thông thường connection pooling sẽ giữ một số lượng connection nhất định, thông thường là 10 đối với công nghệ hiện tại hỗ trợ connection pooling.
Tại sao cần đến Connecion Pooling?
Để trả lời câu hỏi này, chúng ta sẽ tiến hành phân tích một vòng đời của một kết nối từ ứng dụng đến database.
- Mở kết nối đến database sử dụng database driver.
- Mở một TCP socket dùng để đọc/ghi dữ liệu
- Đọc/Ghi dữ liệu từ socket.
- Đóng kết nối.
- Đóng socket.
Chúng ta có thể thấy một loạt các hoạt động cần thiết để tạo ra một kết nối đến database như vậy hẳn sẽ tốn khá nhiều tài nguyên của ứng dụng. Vì vậy những hành động này nên được giảm đến mức tối thiểu trong mọi trường hợp sử dụng có thể.
Từ đây, khái niệm về connection pooling trở nên mạnh mẽ và phát huy tác dụng. Hiểu đơn giản nó sẽ triển khai một vùng chứa kết nối đến cơ sở dữ liệu, cho phép chúng ta sử dụng lại các kết nối hiện có, từ đó có thể giảm thiểu đáng kể các hoạt động hao tốn nhiều tài nguyên và làm cho hiệu xuất của ứng dụng trở nên hiệu quả hơn.
JDBC connection pooling
Bất kỳ một connection pooling framework nào cũng đều phải tuân thủ các bước sau kể cả JDBC connection pooling:
- Khởi tạo connection object.
- Quản lý các connection object.
- Huỷ các connection object khi cần thiết.
Trong Java chúng ta có một bộ các thư viện hỗ trợ connection pooling mạnh mẽ được phát triển từ lâu như:
- Apache Commons DBCP 2
- HikariCP
- C3P0
Để sử dụng những thư viện này chúng ta chỉ cần tiến hành một vài bước cấu hình đơn giản là có thể sử dụng được.
Để bắt đầu tìm hiểu các thư viện ở trên, trước tiên chúng ta cùng chuẩn bị database để làm ví dụ. Các bạn có thể sử dụng đoạn script sau để tạo database empdb.
create database empdb; use empdb; create table tblemployee( empId integer AUTO_INCREMENT primary key, empName varchar(64), dob date, designation varchar(64) ); insert into tblemployee(empId,empName,dob,designation) values (default,'Adam','1998-08-15','Manager'); insert into tblemployee(empId,empName,dob,designation) values (default,'Smith','2001-01-11','Clerk'); insert into tblemployee(empId,empName,dob,designation) values (default,'James','1996-03-13','Officer');
Yêu cầu hệ thống: trong bài viết này mình sử dụng MySQL database, Intellij và JDK 1.8 và maven để build và quản lý các dependency. Nếu các bạn sử dụng những phiên bản khác kể trên thì có thể gây lỗi nha.
Apache commons DBCP 2
Hãy bắt đầu với Apache Commons DBCP Component, một JDBC connection pooling đầy đủ các tính năng.
Trước tiên các bạn cần tạo project maven và thêm dependency của DBCP 2 vào
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.7.0</version> </dependency>
Apache DBCP 2.0 cung cấp 2 loại DataSource: BasicDataSource and PoolingDataSource. Nếu như các bạn chưa biết thì DataSource là nơi lưu trữ các thông tin dùng để kết nối đến database server như username, password, database name etc.
BasicDataSource: Giống như tên gọi, nó đơn giản và phù hợp với hầu hết các trường hợp sử dụng của chúng ta. Trong nội bộ BasicDataSource nó sẽ tạo ra một PoolingDataSource để sử dụng.
Chúng ta có các bước sau để khởi tạo một connection pooling:
- Khởi tạo một BasicDataSource
-
Chỉ định Url JDBC, user name và password để kết nối đến database.
- Chỉ định số lượng connection tối thiểu cần duy trì mọi lúc trong khi ứng dụng chạy.
- Chỉ định số lượng connection tối đa cần duy trì mọi lúc trong khi ứng dụng đang chạy
- Chỉ định tổng số connection tối đa có thể có.
import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import org.apache.commons.dbcp2.BasicDataSource; public class DBCP2Demo { private static BasicDataSource dataSource = null; static { dataSource = new BasicDataSource(); dataSource.setUrl("jdbc:mysql://localhost:3306/empdb?useSSL=false"); dataSource.setUsername("root"); dataSource.setPassword("root"); dataSource.setMinIdle(5); dataSource.setMaxIdle(10); dataSource.setMaxTotal(25); } public static void main(String[] args) throws SQLException { Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { connection = dataSource.getConnection(); statement = connection.createStatement(); resultSet = statement.executeQuery("select * from tblemployee"); while (resultSet.next()) { System.out.println("empId:" + resultSet.getInt("empId")); System.out.println("empName:" + resultSet.getString("empName")); System.out.println("dob:" + resultSet.getDate("dob")); System.out.println("designation:" + resultSet.getString("designation")); } } finally { resultSet.close(); statement.close(); connection.close(); } } }
Output
empId:1 empName:Adam dob:1998-08-15 designation:Manager empId:2 empName:Smith dob:2001-01-11 designation:Clerk empId:3 empName:James dob:1996-03-13 designation:Officer
HikariCP
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>3.4.5</version> </dependency>
Cấu hình HikariCP:
Chúng ta có thể cấu hình HikariCP dễ dàng trong Java Code với những thuộc tính sau:
- idleTimeout: Thời gian tính bằng mili giây mà connection object có thể tồn tại khi không hoạt động.
- connectionTimeout: Thời gian tính bằng mili giây mà client sẽ đợi cho một connection object. Nếu đạt đến giới hạn thời gian thì SQL Exception sẽ được ném ra.
- autoCommit: Chúng ta có thể chỉ định true hoặc false, nếu giá trị là true nó sẽ tự động commit mọi mệnh đề SQL bạn thực thi, nếu false bạn cần phải commit một cách thủ công.
- cachePrepStmts: Bộ nhớ đệm cho Prepare Statement.
- minimumIdle: Số lượng connect tối thiểu cần duy trì mọi lúc trong thời gian ứng dụng chạy.
- maximumPoolSize: Số lượng connection tối đa có thể giữ.
import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class HikariCPDemo { private static HikariDataSource dataSource = null; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/empdb"); config.setUsername("root"); config.setPassword("root"); config.addDataSourceProperty("minimumIdle", "5"); config.addDataSourceProperty("maximumPoolSize", "25"); dataSource = new HikariDataSource(config); } public static void main(String[] args) throws SQLException { Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { connection = dataSource.getConnection(); statement = connection.createStatement(); resultSet = statement.executeQuery("select * from tblemployee"); while (resultSet.next()) { System.out.println("empId:" + resultSet.getInt("empId")); System.out.println("empName:" + resultSet.getString("empName")); System.out.println("dob:" + resultSet.getDate("dob")); System.out.println("designation:" + resultSet.getString("designation")); } } finally { resultSet.close(); statement.close(); connection.close(); } } }
Output
empId:1 empName:Adam dob:1998-08-15 designation:Manager empId:2 empName:Smith dob:2001-01-11 designation:Clerk empId:3 empName:James dob:1996-03-13 designation:Officer
C3P0
C3P0 là một trong những thư viện lâu đời nhất. Nó được sử dụng với Hibernate. Để sử dụng C3P0, chúng ta cần thêm dependency sau vào dự án.
<dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.5</version> </dependency>
Chúng ta có một số cấu hình sau cần lưu ý:
- driverClass: JDBC driver.
- jdbcUrl: Chuỗi kết nối database.
- initialPoolSize: Số lượng connection được tạo lúc khởi chạy ứng dụng.
- acquireIncrement: Số lượng connection mới cần được tạo khi kích thước hiện tại không đủ.
- maxIdleTime: Thời gian sống của một connection khi không còn được sử dụng.
- maxPoolSize: Số lượng connection tối đa có thể giữ.
- minPoolSize: Số lượng connection tối thiểu cần duy trì.
import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import com.mchange.v2.c3p0.ComboPooledDataSource; public class C3P0Demo { static ComboPooledDataSource comboPooledDataSource = null; static { comboPooledDataSource = new ComboPooledDataSource(); comboPooledDataSource.setJdbcUrl("jdbc:mysql://localhost:3306/empdb?useSSL=false"); comboPooledDataSource.setUser("root"); comboPooledDataSource.setPassword("root"); comboPooledDataSource.setMinPoolSize(3); comboPooledDataSource.setAcquireIncrement(3); comboPooledDataSource.setMaxPoolSize(30); } public static void main(String[] args) throws SQLException { Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { connection = comboPooledDataSource.getConnection(); statement = connection.createStatement(); resultSet = statement.executeQuery("select * from tblemployee"); while (resultSet.next()) { System.out.println("empId:" + resultSet.getInt("empId")); System.out.println("empName:" + resultSet.getString("empName")); System.out.println("dob:" + resultSet.getDate("dob")); System.out.println("designation:" + resultSet.getString("designation")); } } finally { resultSet.close(); statement.close(); connection.close(); } } }
Triển khai một Connection Pooling đơn giản
Để hiểu rõ hơn về connection pooling, chúng ta sẽ tiến hành triển khai một connection pooling đơn giản để hiểu rõ cơ chế hoạt động.
Trước tiên chúng ta cần thiết kế một interface định nghĩa các API sử dụng trong connection pooling:
public interface ConnectionPool { Connection getConnection(); boolean releaseConnection(Connection connection); String getUrl(); String getUser(); String getPassword(); }
Tiếp theo chúng ta sẽ tạo một class triển khai ConnectionPool interface trên
public class BasicConnectionPool implements ConnectionPool { private String url; private String user; private String password; private List<Connection> connectionPool; private List<Connection> usedConnections = new ArrayList<>(); private static int INITIAL_POOL_SIZE = 10; public static BasicConnectionPool create( String url, String user, String password) throws SQLException { List<Connection> 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); } // constructor @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(); } // các hàm getter }
Ở trên, chúng ta đã tạo các connection pool lưu trữ trong ArrayList lưu trữ 10 connection có thể sử dụng lại.
Khi các connection được trả lại, nó sẽ được giữ lại trong connectionPool và được sử dụng lại trong những lần tiếp theo.
Lưu ý rằng các bạn có thể DriverManager class và DataSource mình không nêu ở trên nhưng các bạn có thể dễ dàng tạo với JDBC Driver. Các bạn có thể tham khảo bài viết sau để có thể triển khai DriverManager cách dễ dàng: https://shareprogramming.net//tao-ket-noi-den-database-su-dung-jdbc/
Chuyện gì xảy ra khi tất cả các connection đều được sử dụng?
Giả sử bạn cấu hình connection pool với số lượng tối đa là 10 connection. Nhưng tại một thời điểm có rất nhiều người dùng sử dụng ứng dụng của bạn và 10 connection đang được sử dụng hết.
Khi một người dùng tiếp theo truy cập vào ứng dụng và yêu cầu của họ cần kết nối đến database, lúc này họ sẽ được sếp vào hàng đợi cho đến khi có 1 trong 10 connection kia được trả lại thì nó sẽ được sử dụng cho yêu cầu của người dùng này.
Hơn nữa bạn có thể cấu hình thời gian đợi tối đa của một request lấy 1 connection trong connection pooling. Nếu quá thời gian này mà chưa lấy được một connection để thay tác thì các bạn có thể chuyển hướng người dùng đến một trang khác thông báo lỗi, và xin người dùng chờ trong giây lát hãy quay lại sau chẳng hạn.
Nguồn tham khảo