Part 03 · Spring Framework & Spring Boot · 3.1.04A

@Transactional: khi nào commit, flush hay rollback?

Annotation không commit tại dòng save(). Spring proxy mở transaction trước method, method chạy trong transaction, rồi proxy mới quyết định commit hoặc rollback khi method thoát.


1. Timeline mặc định

caller → proxy → BEGIN → target method
                           save/change entities
caller ← proxy ← COMMIT/ROLLBACK ← return/throw

commit thường gồm: flush persistence context → DB COMMIT → release connection

Với JPA, save() thường chỉ đưa entity vào persistence context. SQL có thể chạy lúc flush, trước query, khi gọi flush()/saveAndFlush(), hoặc lúc commit. SQL đã chạy chưa đồng nghĩa đã commit: session khác thường chưa thấy dữ liệu và rollback vẫn có thể hủy thay đổi.

2. Return bình thường: Spring cố commit

@Transactional
public Transfer transfer(long fromId, long toId, BigDecimal amount) {
  Account from = accounts.findByIdForUpdate(fromId);
  Account to = accounts.findByIdForUpdate(toId);
  from.debit(amount);
  to.credit(amount);
  return transfers.save(Transfer.posted(fromId, toId, amount));
} // proxy flushes and commits after method returns

Điều kiện là call đi từ bean khác qua proxy, transaction không rollback-only, và flush/DB commit thành công. Constraint violation có thể chỉ xuất hiện ở flush cuối; method body đã chạy hết nhưng caller vẫn nhận exception.

3. Runtime exception: rollback mặc định

@Transactional
public void transfer(...) {
  debit();
  credit();
  if (limitExceeded()) throw new TransferLimitExceededException();
} // RuntimeException đi qua proxy → rollback

Mặc định Spring rollback với RuntimeExceptionError. Exception phải thoát qua transactional proxy hoặc code chủ động đánh dấu rollback-only.

4. Checked exception mặc định có thể commit

@Transactional(rollbackFor = IOException.class)
public void importTransfers(Path file) throws IOException {
  saveRows();
  Files.move(file, archivePath);
}

Nếu bỏ rollbackFor, checked exception như IOException mặc định không yêu cầu rollback. noRollbackFor chỉ nên dùng khi exception thật sự là outcome vẫn cho phép commit và đã có test.

5. Catch rồi nuốt exception: thường commit

@Transactional
public void wrong() {
  repository.save(entity);
  try {
    auditClient.call();
  } catch (RuntimeException ex) {
    log.warn("ignored", ex);
  }
} // return bình thường → Spring cố commit

Muốn rollback, cách rõ nhất là rethrow exception phù hợp. Nếu bắt buộc phải trả result thay vì throw, có thể đánh dấu transaction rollback-only, nhưng caller phải biết operation đã thất bại.

@Transactional
public TransferResult handledButRollback() {
  try {
    performTransfer();
    return TransferResult.success();
  } catch (BusinessFailure ex) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    return TransferResult.failed(ex.getMessage());
  }
}

6. “Chỉ commit khi done”

Đặt transaction ở public application-service method bao trùm toàn bộ thay đổi database tạo nên một invariant. Không tách debit, credit và transfer record thành ba transaction độc lập.

@Transactional
public void completeTransfer(Command cmd) {
  validate(cmd);
  ledger.postDebit(cmd);
  ledger.postCredit(cmd);
  outbox.append(event(cmd));
} // commit cả ledger và outbox

Remote API chậm không nên nằm trong DB transaction dài. Dùng transaction ngắn ghi PENDING cùng Outbox rồi commit; worker gọi remote và transaction khác ghi outcome. “Done” của distributed workflow cần state machine, idempotency và reconciliation.

7. Những cách viết không tạo boundary mong đợi

Cách viếtKết quả
this.inner() cùng classKhông qua proxy; annotation ở inner không mở propagation mới.
private @TransactionalKhông phải proxy entry point thông thường.
new Service()Không phải Spring bean, không có proxy.
@PostConstructKhông phải entry path thích hợp để dựa vào transactional proxy.
@Async hoặc thread mớiTransaction context của caller không tự truyền sang.

Cách sửa self-invocation tốt nhất là tách operation sang bean khác rồi inject bean đó.

8. Propagation quyết định transaction nào commit

PropagationÝ nghĩa
REQUIREDJoin hiện tại hoặc tạo mới; outer kết thúc mới commit physical transaction chung.
REQUIRES_NEWSuspend outer, transaction độc lập; inner có thể commit dù outer rollback.
NESTEDSavepoint nếu resource hỗ trợ; outer rollback vẫn hủy tất cả.
SUPPORTSJoin nếu có, nếu không chạy không transaction.
MANDATORYFail nếu caller chưa có transaction.
NOT_SUPPORTED/NEVERSuspend hoặc từ chối transaction.
@Transactional
public void placeOrder() {
  orderRepository.save(order);
  auditService.writeAudit(); // bean khác, REQUIRES_NEW
  throw new RuntimeException();
} // order rollback; audit có thể đã commit
Resource caveat: REQUIRES_NEW cần connection khác và có thể gây pool starvation; không dùng propagation này để chia vụn business invariant.

9. Flush không phải commit

@Transactional
public void create(User user) {
  repository.saveAndFlush(user); // phát SQL/kiểm tra constraint sớm
  throw new RuntimeException();  // vẫn rollback được
}

10. Programmatic transaction

Transfer saved = transactionTemplate.execute(status -> {
  Transfer transfer = repository.save(Transfer.pending(command));
  outbox.save(Event.created(transfer));
  return transfer;
}); // commit trước remote I/O

remoteGateway.submit(saved.id());

TransactionTemplate hữu ích cho transaction ngắn trước/sau I/O hoặc mỗi item trong batch. Callback return bình thường thì commit; runtime exception thì rollback; code cũng có thể gọi status.setRollbackOnly().

11. Commit vẫn có thể thất bại

Flush hoặc commit có thể gặp unique/FK constraint, optimistic-lock conflict, deadlock victim, serialization failure, timeout, connection loss hoặc database failover. Chỉ trả success sau khi proxy commit thành công. Nếu mất network đúng lúc commit, outcome có thể bất định; operation quan trọng cần idempotency và query/reconciliation.

12. Event và side effect

13. Test semantics

14. Câu trả lời phỏng vấn ngắn

Khi nào commit? Call đi qua proxy, method hoàn tất bình thường, transaction không rollback-only, rồi flush và DB commit thành công.
Khi nào không commit? Không có boundary thật, rollback rule/rollback-only được kích hoạt, outer transaction rollback, hoặc flush/commit thất bại.
save() có commit không? Không nhất thiết; ngay cả SQL đã flush cũng chưa phải commit.
Nguồn tham khảo