Apache Derby Bài 40: Tích hợp Apache Derby với Connection Pool (HikariCP) để tối ưu hiệu năng
1. Vấn đề "Nút thắt cổ chai" của kết nối mạng
Như đã đề cập ở Bài 26, trong kiến trúc Network Server, mỗi lần ứng dụng gọi lệnh DriverManager.getConnection(), hệ thống phải trải qua một chuỗi các thao tác cực kỳ đắt đỏ:
-
Phân giải địa chỉ IP và thực hiện bắt tay 3 bước (TCP 3-way handshake) qua mạng.
-
Trao đổi thông điệp xác thực (Authentication) tài khoản và mật khẩu.
-
Derby Engine cấp phát một luồng (Thread) mới và một vùng nhớ đệm riêng rẽ trên máy chủ.
Nếu hệ thống backend của bạn phục vụ hàng nghìn giao dịch mỗi giây, việc mở và đóng kết nối liên tục sẽ vắt kiệt CPU của máy chủ, làm tăng độ trễ (latency) của API lên mức không thể chấp nhận được.
Connection Pool (Hồ chứa kết nối) là giải pháp tiêu chuẩn công nghiệp cho bài toán này. Thay vì tạo mới và hủy kết nối liên tục, ứng dụng sẽ khởi tạo sẵn một lượng kết nối nhất định (ví dụ: 20 kết nối) và giữ chúng luôn mở. Khi cần truy vấn, ứng dụng "mượn" một kết nối từ hồ chứa. Dùng xong, thay vì đóng lại (conn.close()), ứng dụng sẽ "trả" nó về hồ chứa để phục vụ cho các luồng xử lý tiếp theo.
2. Tại sao lại là HikariCP?
Trong hệ sinh thái Java, có nhiều thư viện Connection Pool (như Apache DBCP2, C3P0), nhưng HikariCP hiện là tiêu chuẩn vàng. Nó siêu nhẹ (chỉ khoảng 130KB), được tối ưu hóa đến mức mã máy (byte-code level) để mang lại tốc độ cực nhanh, và hiện đang là bộ Pool mặc định được tích hợp sẵn trong Spring Boot.
HikariCP kết hợp hoàn hảo với Apache Derby Network Server để xây dựng một backend vững chắc và chịu tải cao.
3. Cấu hình thư viện Maven/Gradle
Để bắt đầu, bạn cần khai báo thư viện HikariCP bên cạnh thư viện derbyclient thông thường.
Nếu dùng Maven (pom.xml):
XML
<dependencies>
<!-- Thư viện Client của Derby -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId>
<version>10.17.1.0</version>
</dependency>
<!-- Thư viện HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.1.0</version>
</dependency>
<!-- Thư viện Logging (HikariCP yêu cầu SLF4J) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
4. Xây dựng lớp quản lý DataSource (Singleton Pattern)
Trong ứng dụng, Hồ chứa kết nối (Connection Pool) chỉ nên được khởi tạo duy nhất một lần khi ứng dụng khởi động và tồn tại xuyên suốt vòng đời của ứng dụng. Mẫu thiết kế Singleton là phù hợp nhất trong trường hợp này (nếu bạn không dùng các framework như Spring).
Tạo class DerbyConnectionPool.java:
Java
package com.hieu.derby.pool;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class DerbyConnectionPool {
private static HikariDataSource dataSource;
// Khối static khởi tạo Pool ngay khi class được nạp vào bộ nhớ
static {
try {
HikariConfig config = new HikariConfig();
// 1. Cấu hình JDBC URL (Chỉ định Network Server)
config.setJdbcUrl("jdbc:derby://localhost:1527/my_company_db");
config.setUsername("admin");
config.setPassword("MatKhauSieuKho123");
// Tùy chọn class Driver (Không bắt buộc với JDBC 4.0 trở lên, nhưng giúp cấu hình rõ ràng hơn)
config.setDriverClassName("org.apache.derby.jdbc.ClientDriver");
// 2. Cấu hình các tham số tối ưu của Pool
// Số lượng kết nối tối đa được mở đồng thời
config.setMaximumPoolSize(20);
// Số lượng kết nối duy trì ngay cả khi hệ thống nhàn rỗi
config.setMinimumIdle(5);
// Thời gian tối đa (mili-giây) ứng dụng phải chờ nếu Pool đã cạn kết nối
config.setConnectionTimeout(30000); // 30 giây
// Thời gian sống tối đa của một kết nối (tránh lỗi rò rỉ bộ nhớ hoặc đứt kết nối mạng ngầm)
config.setMaxLifetime(1800000); // 30 phút
// Khởi tạo Hồ chứa
dataSource = new HikariDataSource(config);
System.out.println("=> Đã khởi tạo thành công HikariCP Connection Pool cho Derby!");
} catch (Exception e) {
System.err.println("Lỗi khởi tạo Connection Pool: " + e.getMessage());
throw new RuntimeException(e);
}
}
/**
* Hàm dùng để "mượn" một kết nối từ Pool
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* Hàm dùng để dọn dẹp và đóng Pool khi tắt ứng dụng
*/
public static void shutdown() {
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
System.out.println("=> Đã đóng HikariCP Connection Pool an toàn.");
}
}
}
5. Sử dụng Connection Pool trong logic nghiệp vụ
Khi áp dụng Connection Pool, mã nguồn truy vấn SQL của bạn gần như không thay đổi, ngoại trừ việc thay DriverManager.getConnection() bằng DerbyConnectionPool.getConnection().
Điều quan trọng nhất là bạn VẪN PHẢI GỌI HÀM conn.close(). Tuy nhiên, do đối tượng Connection lúc này là một lớp bọc (Proxy) của HikariCP, hàm close() sẽ không thực sự đóng kết nối TCP, mà chỉ đánh dấu kết nối đó là "rảnh rỗi" để trả lại vào Hồ chứa.
Java
package com.hieu.derby.pool;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class App {
public static void main(String[] args) {
// Mượn kết nối từ Pool thông qua khối try-with-resources để tự động "trả" kết nối
try (Connection conn = DerbyConnectionPool.getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT id, ho_ten FROM nhan_vien");
ResultSet rs = pstmt.executeQuery()) {
System.out.println("Tiến hành truy vấn dữ liệu...");
while (rs.next()) {
System.out.println("Nhân viên: " + rs.getString("ho_ten"));
}
// Khi kết thúc khối try này, conn.close() tự động được gọi
// HikariCP sẽ thu hồi kết nối để tái sử dụng.
} catch (Exception e) {
e.printStackTrace();
} finally {
// Đảm bảo đóng hoàn toàn Pool khi tắt ứng dụng
DerbyConnectionPool.shutdown();
}
}
}
6. Hiệu năng thực tế
Nếu bạn viết một đoạn vòng lặp gọi hàm thực thi SQL 10,000 lần:
-
Dùng
DriverManager(Non-pool): Sẽ mất vài chục giây đến vài phút vì phải thiết lập 10,000 kết nối TCP riêng biệt, hệ thống Derby phải tạo và hủy 10,000 luồng. -
Dùng HikariCP: Thời gian rút xuống chỉ còn chưa tới 1 giây, vì 10,000 truy vấn đó được luân chuyển mượt mà trên 20 kết nối TCP luôn được giữ mở ngầm phía dưới.
Đến đây, bạn đã nắm vững cách tối ưu tầng giao tiếp giữa Java và Derby thông qua JDBC nguyên thủy. Tuy nhiên, việc phải viết các câu lệnh SQL chuỗi cứng (hard-code) và map dữ liệu vào Object theo cách thủ công (ResultSet) tốn rất nhiều công sức bảo trì. Bài tiếp theo sẽ nâng cấp kiến trúc của ứng dụng lên một tầm cao mới: Apache Derby Bài 41: Tích hợp Apache Derby với ORM Framework (Hibernate/JPA).
All Rights Reserved