Bài 8: Transactional và AOP

hoangkimhoangkim
4 min read

Mục tiêu bài học

  • Hiểu cách @Transactional trong Spring Boot dựa trên AOP (proxy-based) để quản lý transaction.

  • Biết cách sử dụng các propagation, isolation level khác nhau và tình huống phù hợp.

  • Nắm rõ self-invocation (khi method gọi chính nó) dẫn đến việc không kích hoạt AOP, cùng cách khắc phục.

  • Thử nghiệm rollback, commit, nested transaction, cùng các lỗi thường gặp (LazyInitializationException, no transaction…).


Nội dung chính


1. Transactional AOP

  • @Transactional là annotation của Spring dùng để khai báo rằng “phương thức này cần chạy trong một transaction”.

  • Bên dưới, Spring sẽ tạo proxy cho bean chứa method có @Transactional. Khi method được gọi:

    1. Proxy mở (hoặc tham gia) transaction sẵn có, tuỳ propagation.

    2. Gọi method gốc → Xử lý DB.

    3. Nếu method chạy thành công, proxy commit transaction. Nếu gặp exception, proxy rollback.

  • Lưu ý: Spring AOP chỉ “chèn” logic transaction vào public method gọi từ bên ngoài (qua proxy).

    • Nếu bạn gọi method có @Transactional từ chính method khác trong cùng class, transaction sẽ không kích hoạt (self-invocation problem).

2. Propagation

  • Propagation xác định cách transaction mới tương tác với transaction hiện có. Một số chế độ phổ biến:

    1. REQUIRED: (mặc định) – nếu đang có transaction, dùng nó; nếu chưa có thì tạo mới.

    2. REQUIRES_NEW: luôn tạo transaction mớitreo (suspend) transaction hiện tại (nếu có).

    3. NESTED: tạo transaction lồng nhau (phụ thuộc transaction cha).

    4. SUPPORTS: nếu có transaction, dùng chung; nếu không, chạy non-transactional.

Ví dụ (pseudo-code):

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // ...
    methodB(); // methodB cũng REQUIRED => vẫn chung 1 transaction
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // transaction riêng
}
  • Ở ví dụ trên, khi methodA gọi methodB:

    • methodA đang có transaction. methodB với REQUIRES_NEW => Tạm treo transaction cũ, mở transaction mới → commit/rollback độc lập.

3. Isolation level

  • Isolation level quyết định cách transaction xử lý đọc/ghi dữ liệu, tránh xung đột:

    1. READ_UNCOMMITTED: đọc cả dữ liệu chưa commit (hiếm dùng).

    2. READ_COMMITTED: chỉ đọc dữ liệu đã commit (mặc định của nhiều DB).

    3. REPEATABLE_READ: đọc lại cùng một query trong cùng transaction sẽ thấy kết quả không thay đổi (nếu DB hỗ trợ).

    4. SERIALIZABLE: cao nhất, khóa nhiều, đảm bảo nhất quán nhưng giảm hiệu năng.

Ví dụ:

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processOrder() {
    // ...
}
  • Tuỳ DB (MySQL, PostgreSQL…) mà isolation level mặc định và cơ chế lock khác nhau.

4. Self-invocation

  • Vấn đề: Khi một class có 2 method: methodA() gọi methodB() trong cùng class, thì:

    • Lời gọi nội bộ không đi qua proxy => @TransactionalmethodB không được áp dụng.
  • Cách khắc phục:

    1. Tách methodB sang class khác => lúc gọi methodB, Spring sẽ qua proxy.

    2. Dùng thủ thuật AopContext (ít khuyến khích): ((ClassName) AopContext.currentProxy()).methodB(). Yêu cầu bật exposeProxy = true.

    3. Thiết kế code: Hạn chế self-invocation, tách logic ra service khác.


5. Ví dụ code chi tiết

  1. Service:

     @Service
     public class OrderService {
    
         @Autowired
         private PaymentService paymentService;
    
         @Transactional
         public void createOrder(Order order) {
             // Lưu order vào DB
             // ...
             // Gọi payment
             paymentService.pay(order);
         }
     }
    
  2. PaymentService:

     @Service
     public class PaymentService {
    
         @Transactional(propagation = Propagation.REQUIRES_NEW)
         public void pay(Order order) {
             // Tạo record payment
             // ...
             // Giả sử ném lỗi => rollback payment, 
             // nhưng order transaction ở OrderService ko rollback (vì transaction tách biệt).
         }
     }
    
  3. Kết quả:

    • Gọi orderService.createOrder(order) => Spring bắt @Transactional (REQUIRED) => transaction T1.

    • Gọi paymentService.pay(order) => REQUIRES_NEW => tạm treo T1, tạo T2.

    • Nếu pay() lỗi => T2 rollback, T1 vẫn commit (tuỳ logic).


6. Troubleshooting Transaction

  1. “No transaction is in progress”:

    • Thường do gọi method self-invocation, hoặc quên @Transactional ở class.

    • Kiểm tra method call chain, bảo đảm method public được gọi “từ bên ngoài”.

  2. LazyInitializationException:

    • Xảy ra khi transaction đóng trước khi ta xử lý lazy data.

    • Cần eager fetch, hoặc chỉ dùng lazy trong transaction, hoặc tách code read.

  3. Chọn propagation sai:

    • method con rollback => method cha vẫn commit. Xem xét cẩn thận logic.
  4. Kiểm tra: Bật log org.springframework.transaction=DEBUG để thấy transaction boundary.


Tóm tắt

  • @Transactional tạo transaction proxy xung quanh method.

  • Propagation (REQUIRED, REQUIRES_NEW, NESTED, ...) xác định cách transaction con “thừa hưởng” hay tạo mới.

  • Isolation level quyết định cách đọc/ghi data, tránh xung đột.

  • Self-invocation không qua proxy => @Transactional không có tác dụng. Phải tách code sang bean khác.

  • Bài 9 tiếp tục với Bảo mật và kiểm soát truy cập bằng AOP.


Câu hỏi thảo luận

  1. Bạn từng gặp lỗi rollback không xảy ra dù method ném exception chưa? Do exception không phải RuntimeException? Hay method private?

  2. Tại sao private/final method không bị AOP “bắt”? Bạn có đề xuất gì nếu muốn transaction hoá logic private?

0
Subscribe to my newsletter

Read articles from hoangkim directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

hoangkim
hoangkim