Spring Transaction
Bài 1: Tổng quan về @Transactional trong Spring - Tại sao cần và nó hoạt động ra sao?
Có thể nói nếu dữ liệu là vàng thì transaction chính là két sắt. Việc kiểm soát những gì được ghi xuống database và rollback nếu có vấn đề là chuyện sống còn. Spring hiểu điều đó, và trao vào tay chúng ta một công cụ cực kỳ mạnh mẽ – annotation @Transactional.
1. Tại sao cần @Transactional?
Đảm bảo "đã làm thì phải tới bến"
Hãy tưởng tượng một hàm chuyển tiền gồm 2 bước:
Trừ tiền tài khoản A
Cộng tiền tài khoản B
Nếu trừ xong mà cộng bị lỗi thì sao? Dữ liệu méo mó, người dùng hoảng loạn, và dev ăn hành. Transaction sẽ đảm bảo một nguyên tắc vàng: Hoặc tất cả cùng thành công, hoặc không có gì xảy ra cả.
Viết code gọn, không rối đầu
Không cần phải manually mở transaction, commit, hay rollback. Chỉ cần @Transactional và Spring lo tất.
Ít bug vặt hơn
Không còn cảnh quên rollback, hay commit sớm quá khiến data sai lệch.
2. @Transactional hoạt động ra sao?
Spring dùng AOP (Aspect-Oriented Programming) để "tàng hình" chèn thêm logic trước và sau khi gọi method được đánh @Transactional. Toàn bộ process như sau:
Spring tạo proxy (đóng vai "người gác cổng") cho bean
Khi gọi method đó, proxy này:
Mở transaction
Chạy method
Nếu method ném exception ➜ rollback
Nếu chạy êm ➜ commit
Note:
Chỉ áp dụng cho method
public.Nếu gọi method
@Transactionaltừ một method khác trong cùng class thì sẽ không có transaction nào được mở. Vì Spring không thể "nhảy vào giữa" lời gọi nội bộ đó.
3. Cách dùng cơ bản
@Transactional
public void transfer(String from, String to, BigDecimal amount) {
accountService.debit(from, amount);
accountService.credit(to, amount);
}
Nếu credit() gặp lỗi, cả giao dịch sẽ bị rollback. Không có chuyện "mất tiền oan".
4. Khi nào transaction bị rollback?
Mặc định:
Rollback khi gặp RuntimeException (unchecked)
Không rollback với Exception thông thường (checked)
Muốn rollback khi gặp checked exception? Cần cấu hình rõ:
@Transactional(rollbackFor = CustomCheckedException.class)
Ngược lại, có thể chặn rollback:
@Transactional(noRollbackFor = SomeWarningException.class)
5. Tóm gọn trong vài dòng:
| Tính năng | Mặc định | Tùy chỉnh được |
| Rollback RuntimeException | Có | Có |
| Rollback checked Exception | Không | rollbackFor |
| Gọi nội bộ trong cùng bean | Không có transaction | Chỉ cách là tách bean hoặc inject chính nó để tạo bean rồi mới gọi method trong cùng class. |
Chỉ áp dụng với method public | Đúng | Không tùy chỉnh được |
Bài 2: Propagation – Quản lý transaction lồng nhau trong Spring
Transaction trong Spring không chỉ đơn giản là mở và đóng. Mọi thứ trở nên phức tạp hơn khi có nhiều class, nhiều service gọi nhau và mỗi cái lại đánh @Transactional. Đây là lúc Propagation xuất hiện – nó điều phối transaction chính – phụ – lồng nhau – tách biệt.
1. Propagation là gì?
Propagation định nghĩa khi một method được gọi trong context của một transaction hiện tại, thì nên xử lý transaction như thế nào:
Dùng lại transaction hiện tại?
Tạo mới?
Tạm ngưng transaction đang có?
Hay chạy ngoài transaction?
Spring cung cấp 7 loại Propagation:
| Mode | Mô tả |
| REQUIRED | (Mặc định) Dùng transaction hiện tại, nếu chưa có thì tạo mới |
| REQUIRES_NEW | Tạm ngưng transaction hiện tại (nếu có), tạo transaction mới |
| NESTED | Tạo transaction lồng trong transaction cha (nếu DB hỗ trợ savepoint) |
| MANDATORY | Bắt buộc phải có transaction, nếu không sẽ throw exception |
| SUPPORTS | Có transaction thì dùng, không có thì chạy bình thường |
| NOT_SUPPORTED | Tạm ngưng transaction nếu đang có, chạy ngoài transaction |
| NEVER | Nếu đang có transaction thì throw exception |
2. Các tình huống thực tế
2.1. REQUIRED gọi REQUIRED – dùng chung transaction
@Service
public class ServiceA {
@Transactional
public void methodA() {
serviceB.methodB();
// nếu methodB() fail → toàn bộ rollback
}
}
@Service
public class ServiceB {
@Transactional
public void methodB() {
// throw new RuntimeException();
}
}
Vì đều là
REQUIRED, nên B dùng chung transaction với A. Nếu B fail → A cũng rollback.
2.2. REQUIRED gọi REQUIRES_NEW - tạo transaction mới
@Service
public class ServiceA {
@Transactional
public void methodA() {
try {
serviceB.methodB(); // chạy transaction mới
} catch (Exception e) {
// swallow exception
}
// vẫn tiếp tục, không rollback
}
}
@Service
public class ServiceB {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
throw new RuntimeException("Lỗi nặng");
}
}
Vì B dùng REQUIRES_NEW nên A không bị ảnh hưởng nếu B fail.
2.3. REQUIRED gọi NESTED
@Service
public class ServiceA {
@Transactional
public void methodA() {
try {
serviceB.methodB();
} catch (Exception e) {
// rollback riêng methodB
}
}
}
@Service
public class ServiceB {
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
throw new RuntimeException("Sub rollback");
}
}
Nếu DB hỗ trợ savepoint,
NESTEDsẽ rollback tới điểm đó.methodAvẫn tiếp tục được.
2.4. MANDATORY nhưng không có transaction
@Service
public class ServiceB {
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
// sẽ lỗi nếu không có transaction bên ngoài
}
}
Gọi methodB() từ controller (không có transaction) sẽ gặp lỗi: No existing transaction found.
2.5. NOT_SUPPORTED để gọi 1 service không muốn bị rollback
@Service
public class AuditService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void saveAuditLog() {
// luôn được commit, bất chấp lỗi bên ngoài
}
}
Dùng khi ghi log, audit... không muốn bị rollback theo business logic.
3. Bảng tổng hợp các loại propagation
| Propagation | Có thể rollback độc lập? | Có tách transaction không? | Đa phần dùng khi nào? |
| REQUIRED | Không | Không | Default, dùng nhiều |
| REQUIRES_NEW | Có | Có | Gửi log, audit, báo lỗi |
| NESTED | Có (nếu DB hỗ trợ) | Có | Một phần cần rollback riêng |
| MANDATORY | Không (bắt buộc phải có) | Không | Đảm bảo được gọi đúng context |
| SUPPORTS | Không | Không | Read-only call |
| NOT_SUPPORTED | (chạy độc lập) | Có | Không muốn bị rollback |
| NEVER | Không | Không | Ngăn lạm dụng transaction |
4. So sánh chi tiết: REQUIRES_NEW vs NOT_SUPPORTED
| Tiêu chí | REQUIRES_NEW | NOT_SUPPORTED |
| Giao dịch hiện tại (nếu có) | Bị tạm ngưng (suspended) | Bị tạm ngưng (suspended) |
| Transaction mới | Tạo transaction mới | Không tạo transaction |
| Khả năng rollback riêng | Có thể rollback độc lập | Không rollback được vì không có transaction |
| Commit | Tự commit (nếu không lỗi) | Mọi thao tác được commit ngay (auto-commit) |
| Ứng dụng thực tế | Ghi log có kiểm soát, gửi message MQ, update trạng thái riêng biệt... | Ghi log, insert tracking, call external API… nhưng không cần rollback |
4.1. Khi nào dùng REQUIRES_NEW
Khi muốn logic bên trong có transaction riêng, để:
Không bị ảnh hưởng bởi transaction cha
Có thể rollback riêng (ví dụ: nếu thất bại thì rollback phần đó thôi)
Ví dụ:
@Service
public class PaymentService {
@Transactional
public void processPayment() {
// 1. Giao dịch chính
updateOrder();
try {
// 2. Giao dịch tách biệt
auditService.saveAuditLog(); // không rollback dù payment fail
} catch (Exception e) {
// swallow lỗi log
}
// 3. Giao dịch chính
chargeCustomer(); // nếu lỗi → rollback phần (1) & (3) nhưng không ảnh hưởng log
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog() {
auditRepository.save(new Audit(...));
}
}
4.2. Khi nào dùng NOT_SUPPORTED
Khi muốn chắc chắn rằng logic bên trong chạy ngoài transaction – thường vì:
Không muốn rollback dù có lỗi bên ngoài
Gọi external API, log debug, lưu thông tin phụ… nơi transaction gây overhead hoặc không cần thiết
Ví dụ:
@Service
public class NotificationService {
@Transactional
public void sendPayment() {
updateDb();
try {
auditService.logNonCritical(); // không rollback dù bên ngoài lỗi
} catch (Exception e) {
// swallow
}
throw new RuntimeException("Giả lập lỗi"); // toàn bộ rollback, nhưng log vẫn còn
}
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void logNonCritical() {
auditRepository.save(new Audit(...)); // auto-commit
}
}
Lưu ý
1. Với NOT_SUPPORTED:
Nếu method bị lỗi trong lúc chạy → không rollback được
Không thể bắt rollback thủ công vì không có transaction!
2. Với REQUIRES_NEW:
Có chi phí mở transaction mới → nên tránh lạm dụng trong vòng lặp lớn
Có thể xảy ra deadlock nếu không kiểm soát kỹ transaction cha và con
Bài 3: isolation – Điều gì xảy ra khi 2 transaction cùng truy cập 1 dữ liệu?
Chuyện nhiều transaction cùng lúc đọc/ghi vào một dòng dữ liệu là chuyện như cơm bữa. Và khi đó, nếu không có chiến lược hợp lý, dữ liệu lưu trữ sẽ loạn như chợ vỡ. Vậy nên – isolation là gì? nó giúp ích gì trong trường hợp này.
1. Isolation là gì?
Isolation là mức độ “cách ly” giữa các transaction – nói cách khác, nó kiểm soát một số tình huống dưới đây:
Transaction A có thấy được dữ liệu chưa commit của Transaction B hay không?
Transaction A đang đọc dữ liệu, thì Transaction B có được ghi hay không?
Có xuất hiện dòng mới trong khi đang quét bảng không?
2. Các loại Isolation trong Spring (theo chuẩn SQL)
Spring JPA mapping từ JDBC/SQL và hỗ trợ 5 mức sau:
| Isolation Level | Ý nghĩa |
| DEFAULT | Theo cấu hình DB (thường là READ_COMMITTED) |
| READ_UNCOMMITTED | Thấy luôn cả dữ liệu chưa commit – cực kỳ nguy hiểm |
| READ_COMMITTED | Chỉ thấy dữ liệu đã commit – an toàn cơ bản |
| REPEATABLE_READ | Đảm bảo cùng 1 query đọc cùng 1 dữ liệu (dù có update sau đó) |
| SERIALIZABLE | Cách ly tuyệt đối – transaction chạy như thể đơn luồng |
3. Các vấn đề thường gặp nếu isolation không hợp lý
| Vấn đề | Mô tả | Giải pháp |
| Dirty Read | Transaction A đọc dữ liệu chưa commit từ B | Dùng READ_COMMITTED trở lên |
| Non-repeatable Read | A đọc dòng X, sau đó B update X, A đọc lại thấy khác | Dùng REPEATABLE_READ trở lên |
| Phantom Read | A đọc các dòng thoả điều kiện, B thêm dòng mới phù hợp điều kiện đó, A đọc lại thấy thêm dòng | Dùng SERIALIZABLE |
4. Tình huống minh hoạ
Vấn đề: Dirty Read
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommitted() {
System.out.println(userRepository.findById(1L));
}
Trong khi đó, một thread khác đang:
@Transactional
public void updateName() {
User u = userRepository.findById(1L);
u.setName("New Name");
// chưa commit
// Thread trên sẽ đọc thấy "New Name" dù chưa commit
}
Giải pháp: READ_COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
Ngăn hoàn toàn dirty read.
Vấn đề: Phantom Read
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void scanList() {
List<User> list1 = userRepository.findAllByAgeGreaterThan(18);
// Trong khi đó, một transaction khác thêm user 19 tuổi
List<User> list2 = userRepository.findAllByAgeGreaterThan(18);
// list2 nhiều hơn list1 → phantom xuất hiện
}
Giải pháp:
@Transactional(isolation = Isolation.SERIALIZABLE)
5. Tóm lại
| Scenario | Isolation nên dùng |
| Giao dịch ngân hàng | SERIALIZABLE (hoặc REPEATABLE_READ nếu performance kém) |
| App đọc ghi cơ bản | READ_COMMITTED |
| App chỉ đọc | READ_COMMITTED hoặc SUPPORTS |
| Truy vấn lớn cần consistency tuyệt đối | SERIALIZABLE |
Luôn hiểu DB mặc định dùng isolation nào (ví dụ MySQL là READ_COMMITTED)
Không nên dùng READ_UNCOMMITTED trừ khi thực sự biết mình đang làm gì
SERIALIZABLE rất mạnh nhưng cực kỳ nặng, dễ gây deadlock
REPEATABLE_READ là mức cân bằng giữa an toàn và hiệu năng
Bài 4: rollbackFor, noRollbackFor - cho từng loại Exception
Đằng sau annotation này là cả một kho vũ khí giúp ae kiểm soát transaction cực kỳ chi tiết: rollback chính xác, timeout khi cần, và cả chế độ chỉ đọc. Hãy cùng khai phá!
rollbackFor và noRollbackFor – Quyết định rollback khi nào?
Mặc định Spring rollback khi nào?
Rollback khi gặp
RuntimeExceptionhoặcErrorKhông rollback khi gặp
Checked Exception
Ví dụ:
@Transactional
public void doStuff() throws IOException {
throw new IOException("Checked, nên sẽ không rollback!");
}
Đây là lỗi phổ biến nhất khiến transaction không rollback như dev mong đợi.
Cách ép rollback với checked exception
@Transactional(rollbackFor = IOException.class)
public void doStuff() throws IOException {
throw new IOException("Giờ thì rollback thật!");
}
Cách ngăn rollback với RuntimeException
@Transactional(noRollbackFor = IllegalArgumentException.class)
public void doStuff() {
throw new IllegalArgumentException("Không rollback đâu!");
}
Có thể kết hợp nhiều exception:
@Transactional(rollbackFor = {IOException.class, SQLException.class},
noRollbackFor = IllegalArgumentException.class)
Tổng kết
| Thuộc tính | Tác dụng | Dùng khi nào? |
rollbackFor | Rollback cả checked exception | Giao tiếp với API throw checked exception |
noRollbackFor | Không rollback cho RuntimeException | Trường hợp lỗi nhẹ, không cần undo |
timeout | Hủy transaction nếu quá lâu | Tránh deadlock, chặn các tiến trình treo |
readOnly | Tối ưu cho truy vấn | Tất cả các service chỉ đọc dữ liệu |
Bài 5: readOnly = true – Có thực sự tối ưu? Cách dùng thế nào cho đúng
Trong thế giới Spring, khi dạo qua đống @Transactional, thỉnh thoảng sẽ thấy vài dòng code như thế này:
@Transactional(readOnly = true)
public List<User> findAllUsers() {
return userRepository.findAll();
}
1. readOnly thật ra làm gì?
Sự thật: Spring KHÔNG can thiệp SQL query. Nó không thay đổi câu lệnh SQL, không giúp truy vấn nhanh hơn về mặt logic.
Vậy nó làm gì?
Mục tiêu chính:
Thông báo cho
TransactionManagerbiết rằng method chỉ đọc → Có thể bỏ qua vài thao tác không cần thiết (ví dụ: dirty checking).Một số JDBC driver và DBMS có thể dùng thông tin này để tối ưu (MySQL, PostgreSQL...).
conn.setReadOnly(true);
Spring sẽ gửi chỉ thị này khi tạo connection. Với một số DB, điều này giúp tránh ghi dữ liệu vào temporary tables, lock, cache unnecessary...
2. readOnly có giúp performance tốt hơn không?
Có... nhưng còn tùy.
| Database | Có tận dụng readOnly? | Ghi chú |
| MySQL | Có | Tránh ghi vào binlog |
| PostgreSQL | Có | Có thể skip một số lock |
| Oracle | Không ảnh hưởng nhiều | |
| H2 | Không hỗ trợ | Vì là in-memory DB đơn giản |
Quan trọng:
readOnly = trueKHÔNG làm câu SQL nhanh hơn, nhưng có thể giúp Spring bỏ qua dirty-checking, DB bớt ghi log, JPA không flush session cuối transaction.
3. Lợi ích cụ thể khi dùng readOnly
Spring Hibernate skip dirty checking
@Transactional(readOnly = true)
public void loadData() {
User u = entityManager.find(User.class, 1L);
u.setName("Hacked!");
// Không bị flush hay update vì readOnly = true
}
Nếu không có
readOnly, Hibernate sẽ phát hiệnsetName()và flush thay đổi vào DB nếu chưa clear session.
4. Dùng readOnly thế nào cho đúng?
Dùng khi:
Chỉ đọc dữ liệu, không có thay đổi
Truy vấn lớn, nhiều bản ghi → Giảm dirty checking
Muốn báo hiệu rõ ràng “method này không nên thay đổi state!”
Không dùng khi:
Có thể thay đổi object trong session
Có thao tác lazy-loading dẫn tới modification
Dùng JDBC thuần hoặc repository custom không quan tâm transaction
5. Ví dụ
Không dùng readOnly, Hibernate vẫn flush
@Transactional
public void getUserAndChange() {
User user = userRepo.findById(1L).get();
user.setName("Oops"); // Hibernate sẽ flush vào DB
}
Với readOnly = true, update sẽ bị bỏ qua
javaSao chépChỉnh sửa@Transactional(readOnly = true)
public void safeRead() {
User user = userRepo.findById(1L).get();
user.setName("Oops"); // Hibernate bỏ qua flush
}
6. Dùng @Transactional(readOnly = true) ở tầng nào?
Service layer: nên dùng ở đây! Rõ ràng, kiểm soát logic.
Repository layer: KHÔNG cần, Spring Data JPA đã tự thêm cho các method
find...,get...,query...
Nếu dùng
@Query(...)để custom truy vấn, Spring vẫn gánreadOnlynếu method bắt đầu bằngfindhoặcget.
7. Tóm gọn
| Câu hỏi | Trả lời |
Có nên dùng readOnly = true? | Có, khi method chỉ đọc |
| Có tăng tốc SQL không? | Không trực tiếp |
| Có tối ưu Hibernate không? | Có, bỏ dirty-checking |
| Dùng ở đâu? | Service layer |
8. Một số tình huống nên cẩn thận
Nếu xài
readOnly = truerồi lạientity.setSomething(...), Hibernate sẽ KHÔNG cập nhật — và ae dev mất công debug lý do tại sao không lưu.Dùng
readOnlynhư tuyên bố logic rõ ràng, như là: “method này chỉ đọc, nếu lỡ sửa data thì cứ mặc kệ đừng flush.”
Bài 6: timeout – Không để transaction treo vô thời hạn
Vấn đề: Khi transaction chạy mãi không dứt…
- Transaction treo → connection giữ luôn → pool kẹt → hệ thống… đứng hình
Transaction không timeout = một quả bom nổ chậm. Và nó đang nổ trong production ae đấy!
1. @Transactional(timeout = x)
@Transactional(timeout = 5) // 5 giây
public void processData() {
// nếu chưa commit sau 5s, rollback tự động
}
Ý nghĩa:
Nếu transaction chưa hoàn thành sau
xgiây → rollback.Đơn vị là giây.
Tác dụng với transaction được Spring quản lý.
Không kiểm soát được khi DB đã nhận câu SQL rồi (lúc này phải nhờ đến JDBC hoặc DB layer để timeout).
2. Cấu hình Spring Transaction Timeout mặc định cho toàn hệ thống
Bạn có thể config global trong application.properties hoặc Java config:
spring.transaction.default-timeout=10
Hoặc:
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager tm = new JpaTransactionManager();
tm.setDefaultTimeout(10); // 10 giây
return tm;
}
3. Nhưng timeout trong @Transactional là chưa đủ!
Đôi khi, câu SQL đã chạy rồi thì Spring không kiểm soát được nữa? Lúc này cần thêm các “lớp bảo vệ” khác (JPA, JDBC, HikariCP)
4. JDBC timeout – khi cần bảo kê ở layer thấp hơn
Nếu dùng Spring JPA, JDBC thì có thể set timeout cho từng query:
@QueryHints(@QueryHint(name = "javax.persistence.query.timeout", value = "3000")) // ms
@Query("SELECT u FROM User u WHERE ...")
List<User> findWithTimeout();
Cái này JPA sẽ truyền xuống JDBC driver, yêu cầu query timeout sau X millis.
5. Cấu hình timeout ở các layer: Spring Transaction, JPA, JDBC, HikariCP
5.1. So sánh tổng quan
| Tầng | Mục tiêu chính | Tác động đến | Cách hoạt động |
| Spring Transaction | Quản lý thời gian sống của transaction | rollback toàn bộ transaction | Dựa vào Spring Transaction Manager |
| JPA Timeout | Giới hạn thời gian thực thi query | ngắt query nếu quá lâu | Truyền xuống JDBC driver |
| JDBC Timeout | Ngắt query trực tiếp | cắt đứt câu SQL đang chạy | API của Statement |
| HikariCP | Quản lý connection pool | bảo vệ pool khỏi kẹt | Cắt hoặc thu hồi connection |
5.2. Spring Transaction Timeout
Vị trí: @Transactional(timeout = x) – mức cao nhất, do Spring quản lý
Mục đích: Rollback transaction nếu method không hoàn tất sau x giây.
Đặc điểm:
| Thuộc tính | Giá trị |
| Mặc định | -1 (vô hạn) |
| Đơn vị | Giây |
| Ảnh hưởng | Chỉ kiểm soát ở mức Spring Transaction |
| Không dừng query nếu JDBC đã bắt đầu chạy |
Không dừng câu SQL nếu nó đã gửi đi. Chỉ rollback nếu vượt quá thời gian cho cả transaction method.
5.3. JPA Query Timeout
Vị trí: Khai báo timeout cho từng query với javax.persistence.query.timeout, JPA sẽ truyền xuống JDBC.
JPA wrapper cao cấp hơn JDBC, JPA sẽ gián tiếp truyền timeout xuống JDBC thông qua JDBC bởi phương thức setQueryTimeout()
@QueryHints(@QueryHint(name = "javax.persistence.query.timeout", value = "3000")) // ms
Mục đích: Dừng query đang chạy nếu vượt quá thời gian cho phép.
Đặc điểm:
| Thuộc tính | Giá trị |
| Mặc định | Không timeout |
| Đơn vị | Milliseconds |
| Ảnh hưởng | Cắt câu SQL nếu query quá lâu |
| Áp dụng từng câu lệnh, không cho toàn transaction |
Dùng
@Transactional(timeout=...)là không đủ nếu query cực lâu (ví dụ: full scan 50 triệu rows).
5.4. JDBC Query Timeout
Vị trí: Dưới cùng, dùng trực tiếp JDBC cắm thẳng vào driver
Statement stmt = connection.createStatement();
stmt.setQueryTimeout(5); // giây
Mục đích: Dừng query đang chạy nếu vượt quá thời gian cho phép (giống như JPA quyre hints timeout bên trên)
Đặc điểm:
| Thuộc tính | Giá trị |
| Đơn vị | Giây |
| Mặc định | 0 = không giới hạn |
| Ảnh hưởng | JDBC driver dừng câu lệnh |
| Rất hiệu quả khi query cực nặng |
JDBC là "vũ khí cuối cùng" nếu JPA không xử lý được timeout (khi này cần dùng JdbcTemplate để xử lý).
5.5. HikariCP Timeout
Vị trí: Config ở application.properties hoặc yaml
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
Mục đích: Kiểm soát lifecycle của connection
| Tham số | Ý nghĩa | Default |
connection-timeout | Thời gian tối đa chờ để lấy 1 connection từ pool | 30s |
idle-timeout | Connection không làm gì thì đóng sau X ms | 10 phút |
max-lifetime | Tuổi thọ tối đa của connection (có thể kill sớm để tránh connection chết âm thầm) | 30 phút |
validation-timeout | Thời gian tối đa để kiểm tra connection còn sống | 5s |
Cái này không cắt transaction hay query, mà cắt connection nếu bị treo hoặc chờ quá lâu.
5.6. Gợi ý cấu hình tối ưu (production)
# Spring Transaction Timeout
spring.transaction.default-timeout=30
# HikariCP – Connection Pool
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
# JPA Query Timeout (dùng trong repo cụ thể)
@QueryHints(@QueryHint(name = "javax.persistence.query.timeout", value = "5000"))
Chú ý: Đừng để connection
max-lifetime> wait_timeout mặc định của DB ( ví dụ MySQL default kill connection sau 8 tiếng)

Ví dụ: Giả sử HikariCP cấu hình như thế này:
hikari.maximumPoolSize=10
hikari.maxLifetime=12h
Có nghĩa là:
Pool giữ một connection đến 12 tiếng.
Nhưng MySQL thì kill connection sau 8 tiếng không hoạt động.
Thảm họa:
App lấy một connection từ pool (vẫn nghĩ là "connection ngon")
Nhưng thật ra MySQL đã kill connection đó âm thầm
App nổ lỗi kiểu
Communications link failure,Broken pipe,Connection reset by peerNgười dùng thì ăn lỗi, dev thì ăn chửi
Với các hệ thống microservice:
Transaction không nên quá 30 giây
Timeout SQL nên nhỏ hơn timeout Transaction
Timeout Transaction nên nhỏ hơn timeout HTTP request
Luôn monitor số lượng active connection – nếu lên cao bất thường → khả năng cao bị treo
Bài 7: Những lỗi phổ biến với @Transactional và cách debug
1. @Transactional không có tác dụng
Hiện tượng:
- Gọi method annotated
@Transactional, nhưng không rollback, hoặc query vẫn thực thi từng câu, không như kỳ vọng.
Nguyên nhân:
- Gọi nội bộ trong cùng class:
@Service
public class UserService {
@Transactional
public void doA() { ... }
public void doB() {
doA(); // @Transactional không hiệu lực
}
}
- Vì Spring quản lý
@Transactionalbằng Spring proxy, nên gọithis.doA()→ không đi qua proxy → annotation bị bỏ qua.
Cách fix:
- Tách method ra service khác và gọi qua bean hoặc tự inject chính nó rồi gọi qua injection.
@Component
public class DoAService {
@Transactional
public void doA() { ... }
}
@Service
public class UserService {
private final DoAService doAService;
public void doB() {
doAService.doA(); // ✅ đi qua proxy
}
}
2. Không rollback khi Exception xảy ra
Hiện tượng:
- Gặp exception, nhưng dữ liệu vẫn commit!?
Nguyên nhân:
- Mặc định, Spring chỉ rollback nếu RuntimeException hoặc Error.
@Transactional
public void doSomething() throws IOException { // Checked exception
throw new IOException("Not rollbacked!");
}
Cách fix:
- Cấu hình rollback với
rollbackFor:
@Transactional(rollbackFor = IOException.class)
public void doSomething() throws IOException {
throw new IOException("Rollback OK!");
}
3. Transaction timeout không có tác dụng
Hiện tượng:
- Đặt
timeout = 2, nhưng method chạy 10s vẫn không bị kill.
Nguyên nhân:
- Spring chỉ timeout trên logic Java – không thể dừng query SQL đang chạy. Nếu câu SQL đã gửi đi, Spring không có quyền can thiệp.
Gợi ý:
Hãy đặt timeout tại JDBC level / DB level (MySQL:
max_execution_time, PostgreSQL:statement_timeout).Hoặc dùng tool như
p6spyđể theo dõi những query nào “đốt thời gian”.
4. Transaction propagation không như mong đợi
Hiện tượng:
- Bạn nghĩ method con có
REQUIRES_NEWsẽ commit riêng biệt → nhưng khi method cha lỗi, method con rollback theo luôn!
Nguyên nhân:
- Cần đảm bảo method con thực sự chạy trong transaction mới và được gọi từ một bean riêng biệt, để Spring tạo proxy đúng.
Fix:
@Component
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() { ... }
}
@Service
public class UserService {
@Autowired AuditService auditService;
@Transactional
public void createUser() {
auditService.saveLog(); // commit riêng
throw new RuntimeException(); // rollback createUser, nhưng log vẫn lưu
}
}
Mẹo debug @Transactional
| Mẹo | Mô tả |
spring.jpa.show-sql=true | In log các câu SQL – giúp thấy lệnh có thực thi không |
spring.jpa.properties.hibernate.format_sql=true | Format SQL dễ đọc hơn |
| Dùng AOP Log Interceptor | Log thời điểm mở – commit – rollback |
Dùng TransactionSynchronizationManager | Check xem hiện có đang trong transaction không |
Dùng @Slf4j hoặc logger trước/sau câu lệnh | Thấy rõ tiến trình logic Java |
Ví dụ debug:
log.info("Before insert");
userRepository.save(...);
log.info("After insert");
Tổng lại là đây: Checklist để tránh chết vì @Transactional
Đừng gọi nội bộ trong cùng class
Chỉ rollback
RuntimeException, muốn rollback thêm → cấu hìnhrollbackForTransaction timeout không dừng được SQL đã gửi
Đặt propagation đúng, gọi qua bean khác nếu muốn hiệu lực
Dùng log và công cụ theo dõi transaction rõ ràng