Bài 8: Transactional và AOP

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:Proxy mở (hoặc tham gia) transaction sẵn có, tuỳ propagation.
Gọi method gốc → Xử lý DB.
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).
- Nếu bạn gọi method có
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:
REQUIRED: (mặc định) – nếu đang có transaction, dùng nó; nếu chưa có thì tạo mới.
REQUIRES_NEW: luôn tạo transaction mới và treo (suspend) transaction hiện tại (nếu có).
NESTED: tạo transaction lồng nhau (phụ thuộc transaction cha).
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:
READ_UNCOMMITTED: đọc cả dữ liệu chưa commit (hiếm dùng).
READ_COMMITTED: chỉ đọc dữ liệu đã commit (mặc định của nhiều DB).
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ợ).
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ọimethodB()
trong cùng class, thì:- Lời gọi nội bộ không đi qua proxy =>
@Transactional
ởmethodB
không được áp dụng.
- Lời gọi nội bộ không đi qua proxy =>
Cách khắc phục:
Tách methodB sang class khác => lúc gọi methodB, Spring sẽ qua proxy.
Dùng thủ thuật AopContext (ít khuyến khích):
((ClassName) AopContext.currentProxy()).methodB()
. Yêu cầu bậtexposeProxy = true
.Thiết kế code: Hạn chế self-invocation, tách logic ra service khác.
5. Ví dụ code chi tiết
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); } }
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). } }
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
“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”.
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.
Chọn propagation sai:
- method con rollback => method cha vẫn commit. Xem xét cẩn thận logic.
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
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?
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?
Subscribe to my newsletter
Read articles from hoangkim directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
