1. Storage Engine Cơ Sở Dữ Liệu Không Tương Thích
Khi tích hợp Spring vào hệ thống, việc lựa chọn đúng storage engine của database là yếu tố nền tảng. Ví dụ tại MySQL, engine MyISAM hoàn toàn bỏ qua cơ chế ACID, dẫn đến các lệnh DML không thể thực hiện commit hoặc rollback. Ngược lại, InnoDB mới cung cấp đầy đủ khả năng kiểm soát giao dịch. Từ phiên bản 5.5.5 trở đi, InnoDB đã trở thành mặc định, nhưng nhiều dự án legacy vẫn vô tình cấu hình nhầm sang MyISAM khiến toàn bộ annotation @Transactional trở nên vô nghĩa.
2. Component Chưa Được IoC Container Nắm Quyền Quản Lý
Spring chỉ áp dụng proxy dynamic cho các object do chính nó khởi tạo và đưa vào bean factory. Nếu một class triển khai logic nghiệp vụ bị thiếu annotation đánh dấu component, framework sẽ bỏ qua nó trong quá trình component scanning.
// Thiếu @Service hoặc @Component
public class PaymentProcessor implements PaymentGateway {
@Transactional
public void executeRefund(TransactionRequest req) {
// Logic chuyển tiền hoàn lại
}
}
Khi class này không nằm trong vùng scan package, Spring AOP sẽ không tạo wrapper proxy, đồng nghĩa với việc cơ chế theo dõi trạng thái giao dịch sẽ không bao giờ được kích hoạt.
3. Phạm Vi Truy Cập Của Phương Thức Không Phải Public
Theo tài liệu chính thức của Spring, cơ chế proxy dựa trên CGLIB hoặc JDK Dynamic Proxy chỉ có thể giám sát các method có modifier public. Việc gán annotation lên method có protected, private hoặc package visibility sẽ không sinh ra cảnh báo compile-time, nhưng runtime thì chúng bị bỏ qua hoàn toàn. Để mở rộng scope sang các non-public method, developer bắt buộc phải switch sang chế độ load-time weaving hoặc compile-time AspectJ.
4. Hiện Tượng Tự Gọi Phương Thức Trong Cùng Một Đối Tượng
Lớp học thuật về AOP thường nhắc đến trường hợp internal call. Khi một method trong class A gọi trực tiếp một method khác cùng class A, JVM sẽ thực thi thông qua tham chiếu this, bypassing bước proxy wrapper.
@Service
public class InventoryService {
public void prepareAndShip(OrderDto dto) {
// Gọi thẳng method bên trong class, bypass proxy
reserveStock(dto.getSku());
}
@Transactional
public boolean reserveStock(String sku) {
// Truy vấn giảm tồn kho
return true;
}
}
Trong ví dụ trên, dù reserveStock có gắn @Transactional, nó vẫn chạy ở chế độ stand-alone. Tương tự, nếu phương thức cha có @Transactional và phương thức con dùng REQUIRES_NEW, khi gọi nội bộ, giao dịch con cũng sẽ không tách biệt. Giải pháp phổ biến nhất là inject chính class đó vào trong nó thông qua constructor hoặc field, sau đó invoke qua bean reference. Một cách tiếp cận khác là tách interface và implementation riêng biệt để đảm bảo call luôn đi qua proxy.
5. Thiếu Định Nghĩa Bean PlatformTransactionManager
Spring không thể tự động suy luận cách quản lý lifecycle của transaction nếu thiếu trung tâm điều khiển. Developer bắt buộc phải expose một bean triển khai interface PlatformTransactionManager trỏ về datasource cụ thể.
@Bean
public PlatformTransactionManager txManager(DataSource primaryDs) {
return new DataSourceTransactionManager(primaryDs);
}
Nếu datasource tồn tại nhưng thiếu transaction coordinator, mọi yêu cầu commit/rollback đều thất bại im lặng.
6. Thuộc Tính Truyền Bá Giao Dịch Đặt Thành NOT_SUPPORTED
Mỗi annotation @Transactional đều nhận tham số propagation. Mặc định là REQUIRED, nhưng khi explicit gán Propagation.NOT_SUPPORTED, container sẽ tạm pause bất kỳ giao dịch đang active nào trước đó và thực thi code dưới dạng plain JDBC connection không có transaction boundary.
@Service
public class ReportGenerator {
@Transactional
public void generateMonthlySummary() {
fetchRawData();
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void fetchRawData() {
// Chạy outside transaction context
}
}
Việc chủ động tách khỏi bối cảnh transaction đương nhiên sẽ làm mất tính nguyên vẹn dữ liệu theo chuẩn ACID.
7. Khối Try-Catch Hấp Thụ Ngoại Lệ Mà Không Re-Throw
Framework Spring chỉ thực hiện rollback tự động khi catch một checked exception hoặc unchecked exception lan truyền lên đến proxy layer. Nếu developer wrap code trong block try-catch-finally và swallow exception (không gọi throw hoặc throwable.fillInStackTrace() nữa), Spring sẽ ghi nhận luồng chạy suôn sẻ và issue lệnh commit.
@Service
public class NotificationSender {
@Transactional
public void dispatchAlert(User target) {
try {
saveAuditLog(target);
sendViaSms(target.getPhone());
} catch (Exception e) {
// Logging but swallowing the stack trace prevents rollback
log.error("Dispatch failed", e);
}
}
}
8. Loại Ngoại Lệ Phát Sinh Không Trùng Default Rollback Policy
Theo quy ước mặc định của Spring, chỉ có RuntimeException và Error mới trigger automatic undo action. Các checked exception (như IOException, SQLException) dù được throw ra sẽ không kích hoạt rollback.
Để sửa behavior này, cần chỉ định rõ ràng trong attribute rollbackFor:
@Transactional(rollbackFor = {SQLException.class, TimeoutException.class})
public void batchUpdateRecords(List<DataRecord> records) {
jdbcTemplate.batchUpdate(QUERY_UPDATE, records);
}
Thuộc tính này chấp nhận mảng các Class thừa kế từ Throwable, giúp developer linh hoạt kiểm soát ranh giới undo/redo.