@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 RuntimeException và Error. 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ết | Kết quả |
|---|---|
this.inner() cùng class | Không qua proxy; annotation ở inner không mở propagation mới. |
private @Transactional | Không phải proxy entry point thông thường. |
new Service() | Không phải Spring bean, không có proxy. |
@PostConstruct | Không phải entry path thích hợp để dựa vào transactional proxy. |
@Async hoặc thread mới | Transaction 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 |
|---|---|
REQUIRED | Join hiện tại hoặc tạo mới; outer kết thúc mới commit physical transaction chung. |
REQUIRES_NEW | Suspend outer, transaction độc lập; inner có thể commit dù outer rollback. |
NESTED | Savepoint nếu resource hỗ trợ; outer rollback vẫn hủy tất cả. |
SUPPORTS | Join nếu có, nếu không chạy không transaction. |
MANDATORY | Fail nếu caller chưa có transaction. |
NOT_SUPPORTED/NEVER | Suspend 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
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
}
flush()đồng bộ persistence context thành SQL trong transaction hiện tại.commitmới kết thúc transaction và làm thay đổi durable/visible theo isolation.IDENTITYcó thể buộc INSERT sớm để lấy ID nhưng chưa commit.- Query với flush mode
AUTOcó thể trigger flush trước khi query.
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
BEFORE_COMMIT: listener fail có thể làm rollback; không làm remote I/O lâu.AFTER_COMMIT: DB đã commit, listener fail không rollback được DB.- Gửi message trước commit có thể gửi dù DB rollback; gửi sau commit có thể mất khi process crash.
- Transactional Outbox cung cấp durable handoff tốt hơn.
13. Test semantics
- Return bình thường: đọc bằng transaction mới và thấy dữ liệu.
- Runtime exception: không còn partial rows.
- Checked exception có và không có
rollbackFor. - Catch-and-swallow khác rollback-only.
- Self-invocation không tạo propagation mới.
REQUIRES_NEWcòn dữ liệu khi outer rollback.saveAndFlush()phát SQL nhưng effect vẫn rollback.- Dùng PostgreSQL/Testcontainers cho constraint, isolation và locking thật.
14. Câu trả lời phỏng vấn ngắn
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.