Tích hợp MySQL
Xem thửLúc 9 giờ 05 sáng thứ Hai, cổng thanh toán của một cửa hàng trực tuyến bắt đầu trả lỗi 500. Log chỉ có một dòng lặp đi lặp lại: HikariPool-1 - Connection is not available, request timed out after 30000ms. Đội vận hành nhìn vào MySQL: CPU 8 phần trăm, không có truy vấn chậm. Họ tăng maximum-pool-size từ 10 lên 100 và lỗi biến mất trong mười phút, rồi quay lại mạnh hơn khi MySQL báo Too many connections. Nguyên nhân thật: một đoạn mã đặt @Transactional lên phương thức vừa ghi đơn hàng vừa gọi API của đối tác giao hàng, và đối tác đó chậm 8 giây. Mỗi request giữ một connection trong 8 giây chỉ để chờ mạng.
Sự cố trên không phải chuyện cấu hình. Nó là hệ quả của việc coi kết nối cơ sở dữ liệu như thứ miễn phí. Bài này đi từ dòng khai báo spring.datasource.url cho tới những gì xảy ra bên trong HikariCP, đo bằng một lab chạy MySQL 8 thật qua Testcontainers, để bạn có thể thiết kế pool, chẩn đoán cạn pool và đặt đúng chiến lược schema cho một dịch vụ Spring Boot dùng MySQL.
Sau bài này, bạn có thể
- Dựng kết nối MySQL cho Spring Boot 4 chỉ bằng starter, driver và biến môi trường; giải thích Spring Boot chọn pool và đọc cấu hình như thế nào.
- Mô tả vòng đời một connection trong HikariCP: mượn, dùng, trả, kiểm tra sống, hết hạn, và đọc các số đo của pool.
- Chọn kích thước pool bằng tính toán (định luật Little, giới hạn máy chủ) thay vì đoán, và chẩn đoán cạn pool bằng metric và log.
- Chọn đúng
ddl-auto, dùng Flyway cho schema và hiểu vì saovalidatebắt lỗi lệch schema ngay lúc khởi động. - Biết các thiết lập riêng của MySQL ảnh hưởng hiệu năng và độ đúng: bộ ký tự, thời gian,
rewriteBatchedStatements, và việcIDENTITYtắt batching của Hibernate.
Kết nối đi qua pool
Cách đọc hình
Đi từ trái sang phải: một request vào service có @Transactional. Transaction manager cần một connection nên nó xin DataSource. DataSource do Spring Boot tạo ra là một HikariDataSource, bên trong có một pool giữ sẵn các connection đã mở tới MySQL. Mở một connection mới tốn nhiều lượt khứ hồi mạng (TCP, xác thực, thiết lập phiên) nên pool mở trước và cho mượn lại. Khi pool hết connection rảnh, request phải xếp hàng. Hai nhánh phụ trên hình: thông tin kết nối đi vào từ biến môi trường, và Flyway thay đổi schema theo phiên bản trước khi ứng dụng nhận request.
Spring Boot dựng DataSource như thế nào
Bạn chỉ khai báo ba thứ: starter JPA, driver MySQL và Flyway.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
spring-boot-starter-data-jpa kéo theo HikariCP, Hibernate và Spring Data JPA. Module flyway-mysql cần thiết vì từ Flyway 10 mỗi cơ sở dữ liệu có module riêng; thiếu nó, Flyway báo không hỗ trợ MySQL.
Bên trong, chuỗi xử lý lúc khởi động như sau. Lớp DataSourceAutoConfiguration thấy có DataSource trên classpath và nhờ DataSourceConfiguration.Hikari tạo một bean HikariDataSource. Các thuộc tính chung (url, username, password) được đọc vào DataSourceProperties; tiền tố spring.datasource.hikari.* được ánh xạ thẳng vào các setter của HikariDataSource. Nếu bạn dùng Testcontainers với @ServiceConnection, một JdbcConnectionDetails thay thế url và thông tin đăng nhập. Hibernate nhận dialect từ chính kết nối nên bạn không cần khai báo hibernate.dialect.
spring:
datasource:
url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME:app}?rewriteBatchedStatements=true
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 10
connection-timeout: 5000
leak-detection-threshold: 20000
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
flyway:
locations: classpath:db/migration
Các giá trị ${DB_USER} và ${DB_PASSWORD} lấy từ biến môi trường của tiến trình. Bạn cũng có thể bỏ hẳn khối url, username, password và đặt biến SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD: Spring Boot gán chúng nhờ relaxed binding (gạch dưới thành dấu chấm, chữ hoa thành chữ thường). Mật khẩu không bao giờ nằm trong kho mã; trong Kubernetes, hãy mount từ Secret, trên máy chủ thường dùng trình quản lý bí mật.
Bên trong HikariCP: một connection sống như thế nào
Những tên lớp dưới đây có thật trong HikariCP 7 và rất hữu ích khi đọc stack trace.
Đang tải sơ đồ…
Các điểm cần nhớ từ sơ đồ:
HikariPool.getConnection(long)gọiConcurrentBag.borrow(timeout, unit). Nếu không có entry rảnh, thread bị chặn đến khi có entry được trả hoặc hết thời gian, khi đó némSQLTransientConnectionException.- Thứ bạn nhận được là
ProxyConnection, một lớp bọc quanh connection thật. Khi bạn gọiclose()trên nó, connection không đóng mà quay lại bag (requite). - Một tác vụ nền
HouseKeeperchạy định kỳ để đóng connection rảnh quáidleTimeout(chỉ khi pool có thể co lại), và bổ sung connection để giữminimumIdle. - Mỗi connection có
MaxLifetimeTaskđể thay thế trước khi hết hạn vàKeepaliveTaskđể kiểm tra sống bằng một lệnh ping nhẹ khi rảnh. Nếu bậtleakDetectionThreshold, mỗi lần mượn còn gắn thêm mộtProxyLeakTask.
Các thông số của pool và giá trị mặc định đã đo
Giá trị dưới đây đọc trực tiếp từ HikariDataSource trong lab với Spring Boot 4.1.1 và HikariCP 7:
| Thuộc tính | Mặc định | Ý nghĩa và khuyến nghị |
|---|---|---|
maximum-pool-size | 10 | Số connection tối đa kể cả đang dùng. Đây là giá trị bạn cần tính chứ không đoán. |
minimum-idle | bằng maximum-pool-size (10) | Pool cố định kích thước. Nên để mặc định để không có độ trễ mở connection lúc tải tăng. |
connection-timeout | 30000 ms | Thời gian một request chờ connection. Quá dài làm thread HTTP kẹt lâu; thường đặt 2000 đến 5000 ms để thất bại nhanh. |
idle-timeout | 600000 ms | Chỉ có tác dụng khi minimum-idle nhỏ hơn maximum-pool-size. |
max-lifetime | 1800000 ms (30 phút) | Phải nhỏ hơn thời gian mà tường lửa, proxy hoặc máy chủ cắt kết nối rảnh. MySQL có wait_timeout mặc định 28800 giây (8 giờ). |
keepalive-time | 120000 ms | Chu kỳ ping connection rảnh để tránh bị thiết bị mạng cắt lặng lẽ. |
validation-timeout | 5000 ms | Thời gian tối đa cho một lần kiểm tra sống. |
leak-detection-threshold | 0 (tắt) | Nếu một connection bị giữ lâu hơn ngưỡng này, Hikari ghi cảnh báo kèm stack trace của nơi mượn. |
pool-name | HikariPool-1 | Đặt tên có nghĩa để lọc log và metric khi có nhiều pool. |
Thử và quan sát
Đang tải Animation…
Thử và quan sát
Trước khi bấm, hãy dự đoán: pool 10 connection, mỗi request giữ connection 5 giây, 12 request đến cùng lúc. Chạy biến thể gọi API ngoài trong transaction và để ý số liệu bên phải: pool đạt 10 trên 10 trong khi MySQL không làm gì. Sau đó chuyển sang biến thể transaction ngắn để thấy cùng một pool phục vụ gọn gàng. Điều cần rút ra: nghẽn ở ứng dụng, đo ở pool, và cách chữa là rút ngắn thời gian giữ connection chứ không phải tăng số connection.
Lab đã chạy: cạn pool tái hiện được
Lab tạo một dịch vụ giữ connection và một dịch vụ truy vấn ngắn, với pool 2 connection và connection-timeout=1000.
@Service
public class SlowService {
private final CustomerRepository customers;
SlowService(CustomerRepository customers) {
this.customers = customers;
}
@Transactional
public long holdConnection(long millis) throws InterruptedException {
long count = customers.count();
Thread.sleep(millis);
return count;
}
public long shortTransaction() {
return customers.count();
}
}
Bài kiểm thử khởi động MySQL 8.0 thật, cho hai thread giữ connection 3 giây rồi gọi truy vấn thứ ba:
@Testcontainers
@SpringBootTest(properties = {
"spring.flyway.locations=classpath:db/migration/l38",
"spring.datasource.hikari.maximum-pool-size=2",
"spring.datasource.hikari.connection-timeout=1000",
"spring.datasource.hikari.leak-detection-threshold=2000"})
class PoolProbeIT {
@Container
@ServiceConnection
static MySQLContainer mysql = new MySQLContainer("mysql:8.0");
@Autowired SlowService slow;
@Test
void exhaustionMessage() throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
List<Future<Long>> futures = new ArrayList<>();
for (int i = 0; i < 2; i++) {
futures.add(pool.submit(() -> slow.holdConnection(3000)));
}
Thread.sleep(300);
assertThatThrownBy(slow::shortTransaction)
.isInstanceOf(CannotCreateTransactionException.class)
.hasRootCauseInstanceOf(SQLTransientConnectionException.class);
for (Future<Long> f : futures) f.get();
pool.shutdown();
}
}
Kết quả đã quan sát. Ngoại lệ ngoài cùng là CannotCreateTransactionException: Could not open JPA EntityManager for transaction, còn nguyên nhân gốc mới là thông điệp có đủ thông tin chẩn đoán:
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available,
request timed out after 1000ms (total=2, active=2, idle=0, waiting=0)
Bốn con số trong ngoặc là bản chụp pool tại thời điểm lỗi: tổng 2, đang dùng 2, rảnh 0, đang chờ 0 (request lỗi đã rời hàng đợi nên không còn được đếm). Khi đọc log sự cố, hãy đọc dòng này trước: active bằng total nghĩa là pool cạn; active thấp mà vẫn timeout nghĩa là có vấn đề khi mở connection (mạng, xác thực, máy chủ từ chối).
Cùng bài kiểm thử, log của leak-detection-threshold=2000 sinh ra hai cảnh báo trong lúc giữ connection:
WARN com.zaxxer.hikari.pool.ProxyLeakTask : Connection leak detection triggered for
com.mysql.cj.jdbc.ConnectionImpl@4840ec68 on thread pool-3-thread-1, stack trace follows
java.lang.Exception: Apparent connection leak detected
INFO com.zaxxer.hikari.pool.ProxyLeakTask : Previously reported leaked connection ... was returned to the pool (unleaked)
Hai điều rút ra. Một: stack trace chỉ ra nơi mượn connection, nên đây là công cụ tìm transaction giữ lâu rất tốt. Hai: cảnh báo này không phải bằng chứng rò rỉ thật, vì ở đây transaction chỉ dài chứ không rò; dòng unleaked sau đó xác nhận connection đã được trả. Hãy đặt ngưỡng cao hơn thời gian transaction hợp lệ dài nhất của bạn.
Đọc sức khỏe pool bằng metric
Với spring-boot-starter-actuator, Spring Boot gắn HikariCP vào Micrometer. Các meter đã quan sát trong lab:
| Meter | Ý nghĩa | Dấu hiệu cần để ý |
|---|---|---|
hikaricp.connections | Tổng số connection hiện có | Thấp hơn max liên tục: pool không được dùng hết, hoặc mở connection thất bại |
hikaricp.connections.active | Đang được mượn | Gần max kéo dài: nguy cơ cạn |
hikaricp.connections.idle | Đang rảnh | Bằng 0 khi tải cao |
hikaricp.connections.pending | Số thread đang chờ connection | Lớn hơn 0 kéo dài là cảnh báo sớm nhất của cạn pool |
hikaricp.connections.acquire | Thời gian chờ để mượn | Tăng trước khi timeout xuất hiện |
hikaricp.connections.usage | Thời gian một connection bị giữ | Đây là độ dài transaction thật; theo dõi phân vị 99 |
hikaricp.connections.creation | Thời gian mở connection mới | Tăng đột ngột: vấn đề mạng hoặc máy chủ |
hikaricp.connections.timeout | Số lần timeout khi mượn | Bất kỳ giá trị nào lớn hơn 0 là sự cố cần điều tra |
hikaricp.connections.max, hikaricp.connections.min | Cấu hình hiện hành | Dùng để vẽ ngưỡng trên biểu đồ |
Để xem nhanh một giá trị, bật management.endpoints.web.exposure.include=metrics rồi gọi GET /actuator/metrics/hikaricp.connections.pending. Trong production, hãy cảnh báo theo pending lớn hơn 0 trong vài phút và usage p99 vượt vài trăm mili giây, chứ đừng chờ có lỗi timeout.
Chọn kích thước pool bằng tính toán
Có hai ràng buộc, và pool phải thỏa cả hai.
Ràng buộc thứ nhất đến từ nhu cầu. Theo định luật Little, số connection đang bận trung bình bằng tốc độ yêu cầu nhân thời gian giữ connection. Một dịch vụ nhận 300 yêu cầu mỗi giây, mỗi yêu cầu giữ connection trung bình 15 ms, cần khoảng 300 nhân 0,015, tức 4,5 connection đang bận. Thêm biên cho đỉnh tải, một pool 10 là dư. Nếu mỗi yêu cầu giữ 2 giây vì gọi API ngoài trong transaction, cùng tốc độ đó cần 600 connection, và không pool nào cứu được.
Ràng buộc thứ hai đến từ máy chủ. MySQL 8 mặc định max_connections là 151 (đã đo trong lab: SELECT @@max_connections trả 151). Tổng của mọi instance phải nhỏ hơn con số này trừ phần dành cho quản trị, giám sát và migration:
số instance x maximum-pool-size <= max_connections - dự trữ
Ví dụ 6 instance với pool 20 cho 120 connection, sát giới hạn; khi tự động mở rộng lên 10 instance, bạn có 200 và máy chủ từ chối kết nối hàng loạt đúng lúc tải cao. Hướng dẫn chính thức của HikariCP khuyến nghị pool nhỏ hơn nhiều người nghĩ, khoảng gấp đôi số lõi CPU của máy chủ cơ sở dữ liệu cộng số ổ đĩa hiệu dụng; lý do là cơ sở dữ liệu chỉ thực sự chạy song song ngần đó truy vấn, phần còn lại chỉ tranh chấp tài nguyên. Pool lớn hơn mức cần thiết làm mọi thứ chậm đi chứ không nhanh lên.
Giữ connection ngắn: ba thủ thuật
- Đẩy lời gọi ngoài ra khỏi transaction. Gọi API, gửi email, đọc tệp lớn trước hoặc sau phương thức
@Transactional, hoặc tách bước ghi thành phương thức riêng. Đây là cách sửa sự cố mở đầu. - Đặt
spring.jpa.open-in-view=false. Khi không đặt, Spring Boot ghi cảnh báo lúc khởi động (đã quan sát trong lab):spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering.Chế độ này giữ EntityManager, và thường cả connection, đến hết bước viết JSON, nên transaction kéo dài ngoài ý muốn. Tắt nó để việc nạp lazy buộc phải xảy ra trong service. - Hoãn việc lấy connection với
spring.datasource.connection-fetch=lazy. Lab đoHikariPoolMXBean.getActiveConnections()trong lúc một phương thức@Transactionalchỉ ngủ, không chạm cơ sở dữ liệu: ở chế độ mặc địnheagerkết quả là 1, ở chế độlazykết quả là 0, vàDataSourceđược bọc thànhLazyConnectionDataSourceProxy. Vớilazy, connection chỉ được mượn khi câu SQL đầu tiên chạy. Đây là lưới an toàn chứ không phải lý do để giữ transaction dài; đổi lại, lỗi hết connection có thể xuất hiện muộn hơn ở giữa phương thức thay vì ở đầu.
Schema: ddl-auto và migration
Giá trị ddl-auto | Hành vi | Dùng ở đâu |
|---|---|---|
none | Không làm gì | Production khi dùng công cụ migration |
validate | So entity với schema, lỗi nếu lệch | Khuyến nghị cho mọi môi trường dùng migration |
update | Hibernate tự thêm bảng và cột | Tránh: không xóa, không đổi tên, không thể lặp lại |
create, create-drop | Tạo (và xóa) schema từ entity | Chỉ cơ sở dữ liệu thử nghiệm tạm thời |
Mặc định của Spring Boot: create-drop với cơ sở dữ liệu nhúng khi không có Flyway hay Liquibase, còn mọi trường hợp khác là none. Với MySQL thật, hãy tự đặt validate để lỗi lệch schema xuất hiện ngay khi khởi động.
Lab chứng minh điều đó. Migration V1__customers.sql đầy đủ:
CREATE TABLE customers (
id bigint NOT NULL AUTO_INCREMENT,
name varchar(120) NOT NULL,
created_at datetime(6) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Khi cho ứng dụng chạy với một migration thiếu cột created_at và ddl-auto=validate, ứng dụng không khởi động và báo đúng chỗ lệch:
org.hibernate.tool.schema.spi.SchemaManagementException:
Schema validation: missing column [created_at] in table [customers]
Đó là điều bạn muốn: sự cố lộ ra ở lúc triển khai, không phải lúc người dùng gọi API. Entity tương ứng:
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Instant createdAt;
protected Customer() { }
public Customer(String name, Instant createdAt) {
this.name = name;
this.createdAt = createdAt;
}
public Long getId() { return id; }
public String getName() { return name; }
public Instant getCreatedAt() { return createdAt; }
}
Quy ước đặt tên: Spring Boot dùng CamelCaseToUnderscoresNamingStrategy, nên createdAt ánh xạ sang cột created_at mà không cần @Column.
Các thiết lập riêng của MySQL
Bộ ký tự. Máy chủ MySQL 8 mặc định utf8mb4 với collation utf8mb4_0900_ai_ci (đã đo). Hãy tạo bảng với utf8mb4, không dùng utf8 (tên cũ chỉ lưu tối đa 3 byte mỗi ký tự). Lab ghi và đọc lại chuỗi Nguyễn Văn An 😀 qua JPA và nhận đúng nguyên văn, kể cả ký tự emoji 4 byte.
Thời gian. Cột datetime(6) giữ micro giây. Lab ghi Instant 2026-10-15T10:15:30.123456Z, đọc lại và so sánh bằng equals trả true. Nếu dùng datetime không có (6), phần phân số bị cắt và phép so sánh sẽ sai. datetime không lưu múi giờ, nên hãy lưu Instant theo UTC và chỉ chuyển múi giờ ở lớp hiển thị.
Batching. Đây là phát hiện đo được đáng giá nhất. Lab chèn 20.000 hàng bằng JdbcTemplate.batchUpdate trong một transaction, trên cùng một máy:
rewriteBatchedStatements | Thời gian 20.000 hàng |
|---|---|
false (mặc định của driver) | 9720 ms |
true | 1002 ms |
Gần mười lần. Với tùy chọn này, Connector/J gộp nhiều câu INSERT của một batch thành một câu nhiều giá trị, giảm số lần khứ hồi mạng. Hãy luôn thêm rewriteBatchedStatements=true vào URL nếu bạn có khối lượng ghi hàng loạt.
Nhưng batching của Hibernate còn một điều kiện khác. Lab đặt hibernate.jdbc.batch_size=50 và lưu 1000 entity có @GeneratedValue(strategy = GenerationType.IDENTITY) bằng saveAll; thống kê Hibernate ghi prepareStatementCount=1000. Mỗi entity một câu INSERT riêng. Lý do: với IDENTITY, cơ sở dữ liệu sinh khóa khi chèn, nên Hibernate phải chạy từng câu lệnh ngay để biết khóa, và không thể gom. MySQL không có sequence, nên khi cần ghi hàng loạt qua JPA, hãy dùng JdbcTemplate.batchUpdate hoặc JdbcClient cho đường ghi nặng. PostgreSQL có sequence nên không gặp giới hạn này; bài sau sẽ đo.
Quyết định thiết kế và đánh đổi
| Tình huống | Khuyến nghị | Lý do |
|---|---|---|
| Dịch vụ CRUD điển hình | Pool 10, timeout 3 đến 5 giây | Đủ cho hàng trăm yêu cầu mỗi giây nếu transaction ngắn |
| Nhiều instance, tự động mở rộng | Tính theo công thức và giới hạn trần số instance | Tránh vượt max_connections |
| Nhiều dịch vụ dùng chung một MySQL | Cấp ngân sách connection cho từng dịch vụ | Một dịch vụ lỗi không làm nghẽn cả hệ thống |
| Tải ghi hàng loạt | JdbcTemplate batch và rewriteBatchedStatements | JPA với IDENTITY không gom được |
| Thread ảo (virtual threads) | Vẫn giữ pool nhỏ | Thread ảo rẻ nhưng connection vẫn khan hiếm; hãy dùng semaphore hoặc timeout để chặn hàng chờ vô hạn |
Khi nào không nên tăng pool: khi pending lớn mà usage cũng lớn. Đó là dấu hiệu transaction giữ connection quá lâu; tăng pool chỉ dời điểm sụp đổ sang MySQL.
Sổ tay xử lý sự cố
- Triệu chứng: timeout
Connection is not availabledù MySQL rảnh. Nguyên nhân: transaction dài hoặc lời gọi ngoài trong transaction. Kiểm tra:hikaricp.connections.usagep99, log leak detection với stack trace. Sửa: rút ngắn transaction, đưa lời gọi ngoài ra khỏi nó. - Triệu chứng:
Too many connectionstừ MySQL. Nguyên nhân: tổng pool các instance vượtmax_connections. Kiểm tra:SHOW STATUS LIKE 'Threads_connected'vàSHOW PROCESSLIST. Sửa: giảm pool, giới hạn số instance, hoặc dùng proxy kết nối. - Triệu chứng:
Communications link failurehoặc connection chết lác đác sau khoảng lặng. Nguyên nhân: tường lửa hoặc cân bằng tải cắt kết nối rảnh trướcmax-lifetime. Sửa: đặtmax-lifetimenhỏ hơn thời gian cắt, giữkeepalive-time. - Triệu chứng: ứng dụng không khởi động với
Schema validation: missing column. Nguyên nhân: entity và migration lệch. Sửa: viết migration mới (không sửa migration đã áp dụng). - Triệu chứng: ghi hàng loạt chậm hơn nhiều so với kỳ vọng. Nguyên nhân: thiếu
rewriteBatchedStatements, hoặc dùngIDENTITYvới JPA. Kiểm tra:generate_statisticsvàprepareStatementCount. Sửa: như phần batching. - Triệu chứng: chữ có dấu hoặc emoji thành dấu hỏi. Nguyên nhân: bảng hoặc cột dùng bộ ký tự không phải
utf8mb4. Sửa: chuyển bảng bằng migration.
Bảo mật và vận hành
- Không đưa mật khẩu vào kho mã; dùng biến môi trường hoặc bí mật của nền tảng. Tạo người dùng cơ sở dữ liệu riêng cho ứng dụng với quyền tối thiểu (không dùng
root), và một người dùng riêng cho Flyway nếu cần quyền DDL. - Bật TLS tới máy chủ (với Connector/J dùng thuộc tính
sslMode), đặc biệt khi cơ sở dữ liệu nằm ngoài mạng riêng. - Đặt tên pool và để lộ metric; đặt cảnh báo theo
pending,timeout,usage. - Giữ migration chỉ thêm tiến: không sửa tệp đã chạy, vì Flyway lưu checksum và sẽ từ chối.
Bài thực hành
- Dựng dự án với ba phụ thuộc trên, migration
V1__customers.sql, entityCustomervà kiểm thử Testcontainers vớiMySQLContainer("mysql:8.0"). - Tái hiện cạn pool: pool 2, timeout 1000, hai thread giữ connection 3 giây. Ghi lại thông điệp lỗi.
- Bật
leak-detection-threshold=2000và tìm stack trace chỉ ra dòng mã mượn connection. - Đo
rewriteBatchedStatementsvới 20.000 hàng như bảng trên và lưu kết quả của bạn. - Cố ý xóa cột
created_atkhỏi migration và xác nhận ứng dụng không khởi động với đúng thông điệp.
Tiêu chí hoàn thành: bạn có một bài kiểm thử xanh chứng minh timeout bằng hasRootCauseInstanceOf(SQLTransientConnectionException.class), một con số đo batch có và không có rewriteBatchedStatements (tỉ lệ ít nhất 3 lần), và một ảnh chụp log Schema validation. Gợi ý nếu bài kiểm thử cạn pool không tái hiện: thread thứ ba phải gọi sau khi hai thread đầu đã mượn xong connection, nên hãy để một khoảng ngủ ngắn.
Tự kiểm tra
- Pool 10 connection, dịch vụ nhận 100 yêu cầu mỗi giây, mỗi yêu cầu giữ connection 120 ms. Pool có đủ không? Nếu mỗi yêu cầu thêm một lời gọi API 900 ms trong transaction thì sao?
- Sáu instance ứng dụng, pool 20 mỗi instance, MySQL
max_connections=151, cần dành 15 connection cho quản trị và giám sát. Cấu hình này có an toàn khi tự động mở rộng lên 8 instance không? - Vì sao tăng
maximum-pool-sizetừ 10 lên 100 thường làm sự cố cạn pool tệ hơn thay vì tốt hơn? hikaricp.connections.pendinglớn hơn 0 nhưnghikaricp.connections.usagep99 rất thấp. Bạn nghi ngờ điều gì?- Một lập trình viên lưu 50.000 entity bằng
saveAlltrên MySQL, đã đặtbatch_size=50mà vẫn chậm. Hai lý do có thể là gì và cách chữa? - Vì sao
ddl-auto=validatekết hợp Flyway an toàn hơnupdate? - Cảnh báo leak detection xuất hiện nhưng connection sau đó được trả về. Đây có phải rò rỉ không? Bạn làm gì với ngưỡng?
Gợi ý đáp án. (1) Cần khoảng 100 nhân 0,12, tức 12 connection đang bận: pool 10 không đủ dù chỉ sát ngưỡng. Với 900 ms thêm, cần khoảng 102 connection nên chắc chắn cạn. (2) 6 nhân 20 là 120, thấp hơn 136 (151 trừ 15), nhưng 8 nhân 20 là 160 vượt giới hạn: không an toàn, cần hạ pool hoặc giới hạn số instance. (3) Máy chủ chỉ chạy song song được ít truy vấn; thêm connection làm tranh chấp tăng và có thể chạm max_connections khi nhiều instance. (4) Việc mở connection chậm hoặc thất bại (xem creation), hay pool vừa bị bão hòa ngắn bởi đỉnh tải; kiểm tra mạng, xác thực và max-lifetime. (5) Thiếu rewriteBatchedStatements=true trong URL, và IDENTITY khiến Hibernate không gom được; chuyển sang JdbcTemplate.batchUpdate cho đường ghi nặng. (6) validate không thay đổi gì, chỉ so sánh và dừng sớm khi lệch; update âm thầm sửa schema theo cách không kiểm soát được. (7) Không nhất thiết: dòng unleaked xác nhận connection được trả; đó là transaction dài. Hãy nâng ngưỡng lên trên thời gian transaction hợp lệ dài nhất, và xử lý các stack trace còn lại.
Điều cần nhớ
- Connection là tài nguyên khan hiếm; độ dài transaction quyết định pool khỏe hay cạn, không phải kích thước pool.
- Tính pool bằng định luật Little và giới hạn của máy chủ, rồi đo bằng
pending,usage,timeout. - Production dùng Flyway và
validate; bí mật đi qua biến môi trường. - Trên MySQL, thêm
rewriteBatchedStatements=truevà nhớIDENTITYtắt batching của Hibernate.
Học liệu chọn lọc
- Spring Boot Reference: SQL Databases
- Spring Boot How-to: Database Initialization
- HikariCP: cấu hình
- HikariCP: About Pool Sizing
- MySQL Connector/J: mã nguồn và tài liệu cấu hình
- The Java Tutorials (Oracle): Establishing a Connection (JDBC)
- Flyway Documentation
- Sách: High-Performance Java Persistence (Vlad Mihalcea), chương về connection management và batching.