Skip to main content

Command Palette

Search for a command to run...

Spring Transaction

Updated
22 min readView as Markdown

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:

  1. Spring tạo proxy (đóng vai "người gác cổng") cho bean

  2. 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 @Transactional từ 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ăngMặc địnhTùy chỉnh được
Rollback RuntimeException
Rollback checked ExceptionKhôngrollbackFor
Gọi nội bộ trong cùng beanKhông có transactionChỉ 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ĐúngKhô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:

ModeMô tả
REQUIRED(Mặc định) Dùng transaction hiện tại, nếu chưa có thì tạo mới
REQUIRES_NEWTạm ngưng transaction hiện tại (nếu có), tạo transaction mới
NESTEDTạo transaction lồng trong transaction cha (nếu DB hỗ trợ savepoint)
MANDATORYBắt buộc phải có transaction, nếu không sẽ throw exception
SUPPORTSCó transaction thì dùng, không có thì chạy bình thường
NOT_SUPPORTEDTạm ngưng transaction nếu đang có, chạy ngoài transaction
NEVERNế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, NESTED sẽ rollback tới điểm đó. methodA vẫ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

PropagationCó thể rollback độc lập?Có tách transaction không?Đa phần dùng khi nào?
REQUIREDKhôngKhôngDefault, dùng nhiều
REQUIRES_NEWGửi log, audit, báo lỗi
NESTEDCó (nếu DB hỗ trợ)Một phần cần rollback riêng
MANDATORYKhông (bắt buộc phải có)KhôngĐảm bảo được gọi đúng context
SUPPORTSKhôngKhôngRead-only call
NOT_SUPPORTED(chạy độc lập)Không muốn bị rollback
NEVERKhôngKhôngNgăn lạm dụng transaction

4. So sánh chi tiết: REQUIRES_NEW vs NOT_SUPPORTED

Tiêu chíREQUIRES_NEWNOT_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ớiTạo transaction mớiKhông tạo transaction
Khả năng rollback riêngCó thể rollback độc lậpKhông rollback được vì không có transaction
CommitTự 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
DEFAULTTheo cấu hình DB (thường là READ_COMMITTED)
READ_UNCOMMITTEDThấy luôn cả dữ liệu chưa commit – cực kỳ nguy hiểm
READ_COMMITTEDChỉ 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 đó)
SERIALIZABLECá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 ReadTransaction A đọc dữ liệu chưa commit từ BDùng READ_COMMITTED trở lên
Non-repeatable ReadA đọc dòng X, sau đó B update X, A đọc lại thấy khácDùng REPEATABLE_READ trở lên
Phantom ReadA đọ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òngDù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

ScenarioIsolation nên dùng
Giao dịch ngân hàngSERIALIZABLE (hoặc REPEATABLE_READ nếu performance kém)
App đọc ghi cơ bảnREAD_COMMITTED
App chỉ đọcREAD_COMMITTED hoặc SUPPORTS
Truy vấn lớn cần consistency tuyệt đốiSERIALIZABLE
  • 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á!

rollbackFornoRollbackFor – Quyết định rollback khi nào?

Mặc định Spring rollback khi nào?

  • Rollback khi gặp RuntimeException hoặc Error

  • Khô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ínhTác dụngDùng khi nào?
rollbackForRollback cả checked exceptionGiao tiếp với API throw checked exception
noRollbackForKhông rollback cho RuntimeExceptionTrường hợp lỗi nhẹ, không cần undo
timeoutHủy transaction nếu quá lâuTránh deadlock, chặn các tiến trình treo
readOnlyTối ưu cho truy vấnTấ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 TransactionManager biế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.

DatabaseCó tận dụng readOnly?Ghi chú
MySQLTránh ghi vào binlog
PostgreSQLCó thể skip một số lock
OracleKhông ảnh hưởng nhiều
H2Không hỗ trợVì là in-memory DB đơn giản

Quan trọng: readOnly = true KHÔ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ện setName() 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án readOnly nếu method bắt đầu bằng find hoặc get.

7. Tóm gọn

Câu hỏiTrả 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 = true rồi lại entity.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 readOnly như 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 x giâ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ầngMục tiêu chínhTác động đếnCách hoạt động
Spring TransactionQuản lý thời gian sống của transactionrollback toàn bộ transactionDựa vào Spring Transaction Manager
JPA TimeoutGiới hạn thời gian thực thi queryngắt query nếu quá lâuTruyền xuống JDBC driver
JDBC TimeoutNgắt query trực tiếpcắt đứt câu SQL đang chạyAPI của Statement
HikariCPQuản lý connection poolbảo vệ pool khỏi kẹtCắ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ínhGiá trị
Mặc định-1 (vô hạn)
Đơn vịGiây
Ảnh hưởngChỉ 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ínhGiá trị
Mặc địnhKhông timeout
Đơn vịMilliseconds
Ảnh hưởngCắ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ínhGiá trị
Đơn vịGiây
Mặc định0 = không giới hạn
Ảnh hưởngJDBC 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ĩaDefault
connection-timeoutThời gian tối đa chờ để lấy 1 connection từ pool30s
idle-timeoutConnection không làm gì thì đóng sau X ms10 phút
max-lifetimeTuổ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-timeoutThời gian tối đa để kiểm tra connection còn sống5s

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 peer

  • Ngườ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ý @Transactional bằng Spring proxy, nên gọi this.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_NEW sẽ 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ẹoMô tả
spring.jpa.show-sql=trueIn log các câu SQL – giúp thấy lệnh có thực thi không
spring.jpa.properties.hibernate.format_sql=trueFormat SQL dễ đọc hơn
Dùng AOP Log InterceptorLog thời điểm mở – commit – rollback
Dùng TransactionSynchronizationManagerCheck xem hiện có đang trong transaction không
Dùng @Slf4j hoặc logger trước/sau câu lệnhThấ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ình rollbackFor

  • Transaction 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

More from this blog

Engineer log

62 posts