Cơ Chế Ngừng Hoạt Động Của Transaction Trong Spring Framework

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ó RuntimeExceptionError 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.

Thẻ: spring-aop transaction-management platformtransactionmanager rollback-policy aspectj-weaving

Đăng vào ngày 30 tháng 8 lúc 19:41