Spring REST API & Data JPA
39/46
Bài 39 / 46~12 phútRelationships & TransactionsLộ trình · chặng 16/21Miễn phí… lượt xem

@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:

  1. Caller gọi vào Proxy: Bean gọi nhận được Proxy đại diện từ ApplicationContext.
  2. 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.
  3. 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ự.
  4. 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 định ROLLBACK.

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 SpringHậu quả thực tế
Unchecked ExceptionRuntimeException, ErrorROLLBACK toàn bộDữ liệu được khôi phục an toàn.
Checked ExceptionException (trừ runtime)COMMIT bình thườngDữ liệu ghi dở dang vẫn lưu vào DB!

Cây quyết định lúc method @Transactional kết thúc: không ném exception thì COMMIT như mong đợi, ném RuntimeException hoặc Error thì ROLLBACK, ném checked exception mà có khai rollbackFor thì cũng ROLLBACK, còn checked exception không khai rollbackFor thì Spring COMMIT và phần dữ liệu đã ghi ở lại trong DB

Java
// 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
}
Khuyến nghị chuẩn mực

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:

Java
@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.

Hai đường gọi tới cùng một method: đường dọc từ caller ngoài class đi xuyên hộp OrderService PROXY nên có BEGIN rồi COMMIT hoặc ROLLBACK bọc quanh, còn đường ngang từ importOrders đang chạy trong chính bean gốc gọi thẳng processOne nên không mở transaction nào và mỗi save tự commit riêng

Ba giải pháp khắc phục Self-Invocation

  1. Tách thành hai Service riêng biệt (Khuyến nghị số 1): Chuyển processOne() sang một OrderProcessingService độc lập. Lời gọi từ OrderService sang bean khác sẽ luôn đi xuyên qua Proxy.
  2. Tự inject chính mình (Self-Injection): Sử dụng @Autowired @Lazy private OrderService self; và gọi qua self.processOne(req).
  3. Đẩ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ì?

  1. Đặ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.
  2. Bắt ngoại lệ bằng try-catch mà không ném lại: Nếu bạn catch exception 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ệnh COMMIT.
  3. 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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Cơ chế nào bên dưới được Spring Framework sử dụng để thực thi annotation @Transactional một cách tự động?
  2. Q2
    Mặ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?
  3. Q3
    Tạ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?
  4. Q4
    Giải pháp kiến trúc sạch nhất để giải quyết vấn đề Self-Invocation trong Spring Service là gì?
  5. 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.

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

Bài tiếp theo

Transaction Propagation, Isolation và Tối Ưu Hóa readOnly