Transaction Propagation, Isolation và Tối Ưu Hóa readOnly
Làm chủ các chiến lược Propagation, 4 cấp độ Isolation ngăn chặn anomaly và kỹ thuật tối ưu hóa hiệu năng đọc dữ liệu với @Transactional(readOnly = true).
TL;DR: propagation thiết lập ranh giới giao dịch: REQUIRED (mặc định) gộp chung vào transaction hiện tại (cùng commit/rollback), trong khi REQUIRES_NEW mở một transaction độc lập (thích hợp cho Audit Log). isolation kiểm soát khả năng nhìn thấy dữ liệu giữa các luồng đồng thời: READ_COMMITTED ngăn Dirty Read, REPEATABLE_READ ngăn Non-repeatable Read, và SERIALIZABLE ngăn Phantom Read. Cấu hình readOnly = true giúp Hibernate tắt Dirty Checking và tối ưu bộ nhớ heap.
Các chiến lược Propagation điều khiển ranh giới Transaction ra sao?
Khi Service A gọi Service B, thuộc tính propagation quyết định B sẽ tham gia vào transaction của A hay mở một transaction độc lập:
| Propagation | Khi Caller chưa có Tx | Khi Caller đang có Tx T1 | Trường hợp áp dụng |
|---|---|---|---|
| REQUIRED (Mặc định) | Tạo Tx T1 mới | Tham gia chung vào T1 | 99% logic nghiệp vụ cần tính toàn vẹn All-or-Nothing. |
| REQUIRES_NEW | Tạo Tx T2 mới | Tạm dừng T1, mở T2 riêng | Ghi Audit Log, gửi thông báo, tác vụ best-effort. |
| NESTED | Tạo Tx T1 mới | Tạo Savepoint trong T1 | Hỗ trợ rollback cục bộ về điểm lưu savepoint. |

Cạm bẫy REQUIRED + try/catch gây UnexpectedRollbackException
Nhiều lập trình viên lầm tưởng việc bọc try-catch quanh lời gọi method con sẽ giúp method cha commit bình thường nếu method con thất bại:
// SAI LAM NGHIEP VU:
@Transactional // T1
public Order placeOrder(OrderRequest req) {
Order order = orderRepo.save(new Order(req));
try {
inventoryService.reserve(req.items()); // REQUIRED: Join T1 va nem loi
} catch (Exception e) {
log.warn("Giu kho that bai nhung van muon luu don hang");
}
return order; // NEM UnexpectedRollbackException TAI DAY!
}
Cơ chế bên dưới:
Khi reserve() ném runtime exception, Spring Transaction Interceptor lập tức đánh dấu cờ rollback-only lên T1 ngay tại thời điểm xảy ra ngoại lệ. Cho dù method cha có catch lỗi, khi kết thúc method và Spring cố phát lệnh COMMIT, nó phát hiện cờ rollback-only và ném ngoại lệ UnexpectedRollbackException.
Khắc phục: Nếu muốn reserve() thất bại mà không ảnh hưởng tới placeOrder(), bắt buộc phải chuyển sang REQUIRES_NEW.

Các cấp độ Isolation ngăn chặn những bất thường nào?
Isolation kiểm soát mức độ cô lập giữa các transaction chạy song song:
| Isolation Level | Dirty Read | Non-repeatable Read | Phantom Read | Đánh giá thực tế |
|---|---|---|---|---|
| READ_COMMITTED | ✅ Đã ngăn | ❌ Có thể bị | ❌ Có thể bị | Mặc định chuẩn của PostgreSQL. Cân bằng tối ưu giữa an toàn và hiệu năng. |
| REPEATABLE_READ | ✅ Đã ngăn | ✅ Đã ngăn | ❌ Có thể bị* | Dùng cho tài chính, kế toán khi đọc số dư nhiều lần trong transaction. |
| SERIALIZABLE | ✅ Đã ngăn | ✅ Đã ngăn | ✅ Đã ngăn | Khóa nghiêm ngặt nhất, throughput giảm mạnh, dễ phát sinh Serialization Failure. |
(PostgreSQL triển khai REPEATABLE_READ qua MVCC nên trên thực tế ngăn chặn được cả Phantom Read).
// Ap dung cho nghiep vu chuyen tien doi hoi tinh nhat quan cao
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) { ... }
Cơ chế bên dưới của thuộc tính readOnly = true là gì?
Khi khai báo @Transactional(readOnly = true), hệ thống kích hoạt 3 cơ chế tối ưu hóa:

- Hibernate tắt Dirty Checking: Bỏ qua bước tạo bản sao snapshot trong bộ nhớ, tiết kiệm tới ~30% dung lượng RAM heap per entity.
- Thiết lập FlushMode.MANUAL: Ngăn chặn Hibernate tự động phát lệnh flush trước mỗi câu truy vấn.
- Gửi tín hiệu tới JDBC Driver: Đặt
connection.setReadOnly(true), cho phép các proxy như PgBouncer tự động điều hướng kết nối tới database Read-Replica.
Cạm bẫy sửa dữ liệu trong readOnly Transaction
Nếu bạn vô tình gọi setter cập nhật entity trong một phương thức readOnly = true:
@Transactional(readOnly = true)
public void updateStatus(Long id) {
Order order = repo.findById(id).orElseThrow();
order.setStatus(PAID); // HIBERNATE BO QUA, KHONG FLUSH UPDATE XUONG DB!
}
Mã nguồn vẫn chạy trơn tru, không có bất kỳ exception nào ném ra, nhưng dữ liệu hoàn toàn không được cập nhật xuống cơ sở dữ liệu (Silent No-Op).
Những cạm bẫy phổ biến cần tránh là gì?
- Dùng REQUIRES_NEW trong vòng lặp lớn: Mỗi lần gọi
REQUIRES_NEWđều tốn tài nguyên xin kết nối mới từ connection pool, dễ gây cạn kiệt pool và làm chậm hệ thống. - Lạm dụng Isolation SERIALIZABLE: Gây nghẽn cổ chai hiệu năng và phát sinh lỗi tranh chấp transaction liên tục.
- Quên ghi đè @Transactional ở tầng method: Khi Service class được đánh dấu
@Transactional(readOnly = true), các method ghi (CUD) quên khai báo@Transactionalriêng sẽ không thể lưu dữ liệu.
Tự kiểm tra kiến thức
- Q1Tại sao khối try-catch ở method cha KHÔNG THỂ cứu vãn giao dịch khi method con dùng Propagation.REQUIRED ném một RuntimeException?
- Q2Chiến lược Propagation nào là phù hợp nhất cho tính năng ghi Audit Log để đảm bảo log luôn được lưu kể cả khi giao dịch nghiệp vụ chính bị lỗi rollback?
- Q3Cấp độ Isolation mặc định của hệ quản trị cơ sở dữ liệu PostgreSQL là gì?
- Q4Thiết lập @Transactional(readOnly = true) mang lại lợi ích kỹ thuật nào trong tầng Hibernate Persistence Context?
- Q5Điều gì xảy ra nếu bạn gọi setter thay đổi thuộc tính của một Entity bên trong phương thức được đánh dấu @Transactional(readOnly = true)?
Bài tiếp theo: Tổng Kết Module 06: Relationships & Transactions
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
@Transactional và AOP Proxy: Cơ Chế Quản Trị Giao Dịch
Bài tiếpTổng Kết Module 06: Quan Hệ Thực Thể và Quản Trị Giao Dịch
Lộ trình Junior Java Backend Developer
Chặng 16/21 · Spring Data JPA — bài 19/20 của chặng
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Thảo luận & góp ý
Hỏi đáp về bài này