Ở phần 1 tôi đã tóm tắt cách chạy code mẫu, giải thích cấu trúc , các file trong code mẫu. Tải mã nguồn ví dụ ở đây. Ở phần 2 tôi sẽ nói chi tiết hơn Transaction và Nested Transaction. Trong code mẫu có nhiều nhóm lệnh đọc ghi vào CSDL và nhiệm vụ của lập trình viên phải đảm tính chất Atomic cho từng nhóm lệnh hoặc tất cả nhóm lệnh.

1. Hỏi khi JPARepository lưu, transaction có được tạo ra hay không?

Để chuẩn bị dữ liệu, tôi có viết method BankService.generateSampleData. Trong phương thức này tôi hoàn toàn không khai báo transaction gì cả. Hãy đặt break point để debug và xem console hiện ra gì nhé.

public void generateSampleData() {
  Account johnAccount = new Account("John", 1000L);
  Account bobAccount = new Account("Bob", 2000L);
  Account aliceAccount = new Account("Alice", 1500L);

  Account tomAccount = new Account("Tom", 100L);
  tomAccount.setState(AccountState.DISABLED);

  accountRepo.save(johnAccount);
  accountRepo.save(bobAccount);
  accountRepo.save(aliceAccount);
  accountRepo.save(tomAccount);
  accountRepo.flush();
}

Ứng với mỗi một lệnh accountRepo.save thực thi thì lại có một implicit transaction được tạo ra. Màn hình Console xuất ra như thế này.

JpaTransactionManager - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
JpaTransactionManager - Opened new EntityManager [SessionImpl(1250363449<open>)] for JPA transaction
TransactionImpl - On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
TransactionImpl - begin
JpaTransactionManager - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@30383db0]
JpaTransactionManager - Initiating transaction commit
JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(1250363449<open>)]
TransactionImpl - committing
JpaTransactionManager - Closing JPA EntityManager [SessionImpl(1250363449<open>)] after transaction

Như vậy một phương thức chứa các lệnh thay đổi dữ liệu, transaction luôn được tạo ra. Nếu chưa khai báo thì implicit transaction (loại ngầm định) được tạo ra. Ngược lại lập trình viên có thể chủ động khai báo transaction bằng annotation @Transaction thì explicit transaction sẽ được tạo ra.

2. Nên sử dụng implicit transaction hay explicit transaction?

Nếu bạn viết một vòng lặp để tạo insert 1000 bản ghi vào CSDL, thì việc không khai báo explicit transaction sẽ khiến cho SpringBoot tạo ra 1000 implicit transaction. Rõ ràng là tốc độ ghi vào CSDL sẽ chậm đi nhiều, bộ nhớ SpringBoot sử dụng cho mỗi implicit transactioncũng tăng.

Ngay cả một câu lệnh truy vấn rất đơn giản, implicit transaction cũng được tạo. Xem thêm giải thích How to use @Transactional with Spring Data?  tóm lại cả 4 lệnh CRUD trong Repository để thực thi trong implicit transaction.

Optional<Account> o_fromAccount = accountRepo.findById(1L);

Tiếp theo, chúng ta sẽ bổ xung annotation @Transactional trên phương thức BankService.generateSampleData

@Transactional(rollbackOn = { Exception.class })
public void generateSampleData()

Điểm khác biệt rõ nhất của explicit transaction, là các lệnh CRUD giờ đây không tạo ra transaction riêng rẽ nữa mà tham gia một transaction context được tạo ngay từ khi phương thức bắt đầu.

JpaTransactionManager - Found thread-bound EntityManager [SessionImpl(28946465<open>)] for JPA transaction
JpaTransactionManager - Participating in existing transaction

Lời khuyên ở đây là bạn nên khai báo explicit transaction để tối ưu theo ý đồ của bạn hơn là để Spring Boot ngầm định tạo ra transaction.

3. Nghiệp vụ của dịch vụ chuyển tiền

  1. Lệnh chuyển tiền sẽ gồm 3 trường: mã tài khoản bị trừ tiền, mã tài khoản được nhận tiền và số tiền. Transaction cần đảm bảo lệnh trừ tiền và nhận tiền phải cùng thành công hoặc cùng bị huỷ bỏ về trạng thái ban đầu.
  2. AllLog: Cần lưu lại mọi lệnh chuyển tiền bất kể nó thành công hay thất bại.
  3. TransactLog: Chỉ lưu lại những lệnh chuyển tiền thành công, bỏ qua những lệnh thất bại

Vậy là có 3 nhóm lệnh thay đổi CSDL. Nhóm 1 và 3 có ghép chung vào một transaction context. Nhóm 2 cần phải hoạt động độc lập bởi khi nhóm 1 thất bại, thì lệnh ghi AllLog vẫn phải được thực thi.

4. Logic của BankService.transfer

  1. Kiểm tra mã tài khoản bị trừ tiền có tồn tại không. Nếu không throw new BankException
  2. Kiểm tra mã tài khoản được nhận tiền có tồn tại không. Nếu không throw new BankException

  3. Kiểm tra số tiền trong tài khoản bị trừ tiền có đủ để chuyển không. Nếu không throw new BankException

  4. Kiểm tra tài khoản nhận tiền có bị phong tảo, hoặc tạm ngưng không. Nếu có throw new BankException

  5. Sau khi thực hiện loạt kiểm tra thì mới tiến hành ghi vào CSDL

@Transactional(rollbackOn = { BankException.class })
  public TransferResult transfer(long fromAccID, long toAccID, long amount) {
    //Các lệnh kiểm tra điều kiện ban đầu. Tạm xoá đi để cho ngắn gọn

    fromAccount.setBalance(fromAccount.getBalance() - amount); //Trừ tiền
    toAccount.setBalance(toAccount.getBalance() + amount); //Cộng tiền
    Date transferDate = new Date();
    TransactLog transactLog = new TransactLog(fromAccount, toAccount, amount, transferDate);

    accountRepo.save(fromAccount); //Lưu xuống CSDL
    accountRepo.save(toAccount);//Lưu xuống CSDL
    transactLogRepo.save(transactLog);  //Log giao dịch thành công
    
    loggingService.saveLog(fromAccID, toAccID, amount, BankErrorCode.SUCCESS, "Success");

    return new TransferResult(BankErrorCode.SUCCESS, "Transfer success", transferDate);
  }  

5. Explicit transaction được tạo ra lúc nào và khi nào commit / rollback?

  1. Khi phương thức có @Transactional bắt đầu thực thi, về cơ bản một transaction context mới được tạo ra. Console sẽ in ra

    Creating new transaction with name [vn.techmaster.bank.service.BankService.transfer]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-vn.techmaster.bank.exception.BankException

  2. Khi một lệnh trong phương thức có @Transactional thay đổi dữ liệu thì lệnh đó tham gia vào transaction context. Console sẽ in ra.
    Participating in existing transaction
  3. Nếu các lệnh đều thành công, quá trình commit sẽ thực hiện. Console sẽ in ra (một số thông tin có thể hơi khác)
    Initiating transaction commit
    JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(1205335461<open>)]
    TransactionImpl - committing

  4. Nếu có lỗi, ngoại lệ phát sinh. Qua trình rollback sẽ thực hiện.
    JpaTransactionManager - Rolling back JPA transaction on EntityManager [SessionImpl(130022483<open>)]
    TransactionImpl - rolling back

Rollback khi Exception được ném ra
Explicit Transaction commit thành công

 

6. Nested Transaction

Ở phần trước, mỗi phương thức được đánh dấu @Transactional thì một explicit transaction context được tạo ra.

Hỏi: một Bean chứa nhiều phương thức được đánh dấu bởi @Transactional. Phương thức này gọi phương thức kia. Vậy transaction context tạo ra trong hai phương thức này sẽ khác biệt nhau hay giống nhau?
Trả lời: các phương thức trong cùng một Bean, khi gọi lẫn nhau, sẽ chỉ có một explicit transaction context được tạo ra.

Hỏi: Nếu buộc phải tạo ra 2 explicit transaction context độc lập (để một transaction thất bại thì transaction kia vẫn hoạt động bình thường) thì phải làm thế nào?
Trả lời: bạn sẽ phải tách 2 phương thức đó, để mỗi phương thức nằm trong bean component riêng biệt.

Trường hợp hai phương thức thuộc một Bean phải dùng chung một transaction context không có gì đáng nói cả. Vì nó luôn là như vậy.

Tôi tạo ra một class LoggingService với chủ đích luôn ghi AllLog bất chất kết quả chuyển tiền thành công hay thất bại.

@Service
public class LoggingService {
  @Autowired
  private AllLogRepo allLogRepo;

  
  //@Transactional(value = TxType.NOT_SUPPORTED, dontRollbackOn={ BankException.class })
  
  @Transactional(value = TxType.REQUIRES_NEW)  //lưu được all log thành công vì tạo ra 2 transaction context khác nhau
  //@Transactional(value = TxType.REQUIRED) //Nằm trong transaction context của hàm gọi, nên không lưu được mọi log
  //@Transactional(value = TxType.REQUIRED, dontRollbackOn={ BankException.class })  
  //@Transactional(value = TxType.SUPPORTS) //Không ghi được hết log
  //@Transactional(value = TxType.NOT_SUPPORTED) //Cũng ghi được nhiều log thành công
  //@Transactional(value = TxType.NEVER) //Báo lỗi Existing transaction found for transaction marked with propagation 'never'
  public void saveLog(long fromID, long toID, Long amount, BankErrorCode resultCode, String detail) {
    allLogRepo.save(new AllLog(fromID, toID, amount, resultCode, detail));
  }
}

6.1 Ý nghĩa của TxtType trong @Transactional

    TxType là một Enum có những tuỳ chọn sau đây:

    1. REQUIRED (mặc định)
    2. REQUIRED_NEW
    3. MANDATORY
    4. SUPPORTS
    5. NOT_SUPPORTS
    6. NEVER

    Mỗi một tuỳ chọn sẽ có 2 khả năng: 

    • TH1: Một transaction nằm ngoài một transaction khác
    • TH2: Một transaction nằm trong một transaction khác, hay còn gọi là Nested Transaction.

    Rõ ràng chúng ta đang ở trường hợp 2. Vậy chúng ta chỉ tìm những tuỳ chọn nào: tạm dừng Transaction hiện tại, tạo mới Transaction Context để thực thi, rồi sau đó lại tiếp tục Transaction bị tạm dừng. Chỉ có 2 tuỳ chọn là TxType.REQUIRES_NEW  TxType.NOT_SUPPORTED. Khác biệt giữa hai tuỳ chọn này là cách thức chúng ứng phó khi được gọi bên ngoài một transaction context. TxType.REQUIRES_NEW sẽ luôn tạo mới transaction context, còn TxType.NOT_SUPPORTED không tạo mới transaction context.

    Case ACase B
    REQUIREDREQUIRED_NEW
    MANDATORYNOT_SUPPORTS
    SUPPORTS 

    TxType.NEVER sẽ ném ra Exception với thông báo "Existing transaction found for transaction marked with propagation 'never'". Có nghĩa là phương thức LoggingService.saveLog không chấp nhận bị gọi ra trong một transaction context có trước đó

    6.2 rollBackOn

    Trong khi thực thi một transaction, nếu có một số loại Exception được ném ra, tôi muốn quay lui về trạng thái đầu, tôi sẽ định nghĩa rollBackOn={ExceptionA.class, ExceptionB.class}. Câu hỏi đặt ra nếu phát sinh một Exception không có trong danh sách rollBackOn được ném ra, điều gì sẽ xảy ra. Trả lời: có 2 khả năng:

    1. Nếu bạn xử lý Exception này rốt ráo, ứng dụng tiếp tục chạy mà không rollback, có thể commit thành công.
    2. Nếu bạn không Exception, ứng dụng sẽ sụp đổ, transaction cũng sẽ rollback mà thôi. Nhưng đừng để tình huồng sự đã rồi này xảy ra.

    6.3 dontRollbackOn

    Trong một phương thức có 10 lệnh sửa đổi dữ liệu. Ứng dụng chạy xong lệnh thứ 5, đang chạy lệnh thứ 6 thì phát sinh ngoại lệ. Tôi muốn ngoại lệ được ném ra ngoài, những không muốn 5 lệnh thành công ban đầu phải rollback lại. Tôi sẽ sử dụng dontRollbackOn chỉ đúng loại ngoại lệ đó.

    Làm một thí nghiệm, khai báo một đoạn lệnh ném ra DummyException

    fromAccount.setBalance(fromAccount.getBalance() - amount);
    
    if (true) {
      throw new DummyException();
    }
            
    toAccount.setBalance(toAccount.getBalance() + amount);

    Phía trên phương thức, định nghĩa lại

    @Transactional(rollbackOn = { BankException.class }, dontRollbackOn = {DummyException.class})
    public TransferResult transfer(long fromAccID, long toAccID, long amount) {
    }

    Khi chạy lệnh chuyển tiền ta sẽ thấy: tiền được trừ thành công ở tài khoản chuyển đi, nhưng lại chưa được cộng vào tài khoản nhận. Điều đó chứng tỏ phương thức vẫn commit những lệnh thay đổi dữ liệu đã thực hiện trước khi DummyException ném ra.

    Với ngoại lệ lửng lơ, không nằm trong  rollBackOn mà cũng chả nằm trong dontRollbackOn, được ném ra, phương thức sẽ thực hiện Rollback nhưng bình thường.

    7. Kết luận

    Chúng ta đã tìm hiểu cơ chế Nested Transaction trong JPA. Đây là một đề xuất của tôi để tối ưu khi làm việc với JPA transaction.

    1. Cần cấu hình tham số TxType, rollBackOndontRollbackOn rõ ràng tránh tình trạng Exception lửng lơ được ném ra.
    2. Khi cần tạo một transaction độc lập với outer transaction nhất thiết phải tạo một Bean mới.
    3. Hạn chế Nested Transaction lồng nhau quá 2 cấp, việc debug và đảm bảo tính toàn vẹn dữ liệu trở nên rất mong manh, khó chắc chắn.
    4. Hãy viết code làm sao để tạo mới transaction muộn nhất và rollback hoặc commit nó càng sớm càng tốt. Hạn chế chạy những lệnh kéo dài thời gian không liên quan đến transaction.