@Transactional và AOP Proxy: Cơ Chế Quản Trị Giao Dịch
Bóc tách cơ chế CGLIB AOP Proxy, quy tắc rollback RuntimeException vs Checked Exception và xử lý cạm bẫy Self-Invocation làm mất tác dụng @Transactional.
TL;DR: @Transactional hoạt động thông qua cơ chế Spring AOP Proxy — Spring tạo ra một CGLIB subclass bao bọc lấy service bean để tự động chèn các lệnh BEGIN, COMMIT, hoặc ROLLBACK. Mặc định Spring chỉ rollback khi gặp RuntimeException hoặc Error, còn Checked Exception vẫn bị commit bình thường. Cạm bẫy Self-Invocation (this.method()) gọi trực tiếp vào instance nội bộ mà không qua proxy, khiến @Transactional hoàn toàn mất tác dụng.
Cơ chế AOP Proxy của @Transactional hoạt động như thế nào?
Thay vì yêu cầu lập trình viên phải mở kết nối JDBC, gọi setAutoCommit(false), commit() và rollback() thủ công, Spring sử dụng kỹ thuật Dynamic Proxy để giải quyết bài toán:
Khi một bean có phương thức gắn @Transactional, Spring tạo ra một lớp con kế thừa bằng CGLIB tại thời điểm khởi động:
- Caller gọi vào Proxy: Bean gọi nhận được Proxy đại diện từ ApplicationContext.
- Kích hoạt TransactionInterceptor: Interceptor can thiệp trước khi method thực thi, lấy Connection từ DataSource và phát lệnh
BEGIN. - Uỷ quyền tới Target Bean: Proxy chuyển tiếp lời gọi tới logic nghiệp vụ thật sự.
- Xử lý kết quả: Nếu hoàn thành suôn sẻ, Proxy phát lệnh
COMMIT. Nếu có ngoại lệ ném ra, Proxy quyết địnhROLLBACK.
Quy tắc Rollback: RuntimeException vs Checked Exception
Đây là nguyên nhân gây ra các lỗi rò rỉ hoặc lưu dữ liệu một nửa (partial data corruption) nghiêm trọng nhất:
| Loại ngoại lệ | Kế thừa từ | Hành vi mặc định của Spring | Hậu quả thực tế |
|---|---|---|---|
| Unchecked Exception | RuntimeException, Error | ROLLBACK toàn bộ | Dữ liệu được khôi phục an toàn. |
| Checked Exception | Exception (trừ runtime) | COMMIT bình thường | Dữ liệu ghi dở dang vẫn lưu vào DB! |

// NGUY HIEM: Checked Exception van bi COMMIT
@Transactional
public void processPayment() throws IOException {
accountRepo.deductMoney(); // Lenh tru tien van luu xuong DB!
throw new IOException("Loi mang khi goi cong thanh toan"); // Spring van COMMIT!
}
// AN TOAN: Khai bao rollbackFor tuong minh
@Transactional(rollbackFor = Exception.class)
public void processPaymentSafe() throws IOException {
accountRepo.deductMoney();
throw new IOException("Loi mang"); // Spring se ROLLBACK
}
Trong thiết kế phần mềm hiện đại, mọi Business Exception của ứng dụng nên kế thừa từ RuntimeException để tận dụng cơ chế auto-rollback tự nhiên của Spring.
Vì sao Self-Invocation lại vô hiệu hóa @Transactional?
Hãy xem xét kịch bản một phương thức không có transaction gọi một phương thức có @Transactional trong cùng một Service class:
@Service
public class OrderService {
// Goi tu Controller vao day
public void importOrders(List<OrderRequest> requests) {
requests.forEach(req -> this.processOne(req)); // LICH GOI NOI BO (SELF-CALL)
}
@Transactional
public void processOne(OrderRequest req) {
orderRepo.save(new Order(req));
inventoryRepo.reserve(req.items());
}
}
Điều gì thực sự xảy ra?
Lời gọi importOrders() đi từ ngoài vào Proxy, nhưng khi vào bên trong thực thể thật, câu lệnh this.processOne(req) là lời gọi nội bộ trên đối tượng Java thuần túy (this), hoàn toàn không đi qua Proxy.
Kết quả: Toàn bộ interceptor của @Transactional trên processOne bị bỏ qua 100%! Nếu lệnh giữ kho thất bại, đơn hàng vẫn được lưu vào database mà không hề bị rollback.

Ba giải pháp khắc phục Self-Invocation
- Tách thành hai Service riêng biệt (Khuyến nghị số 1):
Chuyển
processOne()sang mộtOrderProcessingServiceđộc lập. Lời gọi từOrderServicesang bean khác sẽ luôn đi xuyên qua Proxy. - Tự inject chính mình (Self-Injection):
Sử dụng
@Autowired @Lazy private OrderService self;và gọi quaself.processOne(req). - Đẩy @Transactional lên phương thức ngoài cùng: Nếu toàn bộ quy trình import được phép chạy trong cùng một transaction lớn duy nhất.
Những cạm bẫy phổ biến cần tránh là gì?
- Đặt @Transactional trên phương thức private: CGLIB Proxy không thể override phương thức private, annotation sẽ bị bỏ qua hoàn toàn.
- Bắt ngoại lệ bằng try-catch mà không ném lại: Nếu bạn
catchexception bên trong method và nuốt lỗi (swallow exception), Proxy sẽ coi như phương thức chạy thành công và phát lệnhCOMMIT. - Quên cấu hình rollbackFor cho Checked Exception: Khiến các lỗi I/O hoặc lỗi nghiệp vụ kiểu checked vô tình commit dữ liệu sai lệch.
Tự kiểm tra kiến thức
- Q1Cơ chế nào bên dưới được Spring Framework sử dụng để thực thi annotation @Transactional một cách tự động?
- Q2Mặc định, Spring @Transactional sẽ kích hoạt hành vi ROLLBACK khi phương thức ném ra loại ngoại lệ nào?
- Q3Tại sao lời gọi nội bộ 'this.myMethod()' (Self-Invocation) lại làm vô hiệu hóa annotation @Transactional đặt trên myMethod?
- Q4Giải pháp kiến trúc sạch nhất để giải quyết vấn đề Self-Invocation trong Spring Service là gì?
- Q5Điều gì sẽ xảy ra nếu lập trình viên đặt annotation @Transactional trên một phương thức private?
Bài tiếp theo: Transaction Propagation, Isolation & Read-Only Optimization
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.
Cascade Operations và Xử Lý Triệt Để Bài Toán N+1 Query
Bài tiếpTransaction Propagation, Isolation và Tối Ưu Hóa readOnly
Lộ trình Junior Java Backend Developer
Chặng 16/21 · Spring Data JPA — bài 18/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