Featured image of post Transaction Là Gì? Cách sử dụng @Transactional trong Spring Boot?

Transaction Là Gì? Cách sử dụng @Transactional trong Spring Boot?

Transaction là một phần quan trọng không thể thiếu khi làm việc ở phía back-end nhằm đảm bảo tính đúng đắn của dữ liệu. Trong bài viết này, chúng ta sẽ tìm hiểu sâu hơn về khái niệm transaction trong database, các thuộc tính ACID đảm bảo tính nhất quán của transaction và cách Spring Boot hỗ trợ chúng ta quản lý transactions một cách hiệu quả.

Transactions là gì?

Có lẽ các bạn làm trong ngành IT cũng ít nhiều nghe qua một vài lần về transaction và tầm quan trọng của nó trong việc quản lý dữ liệu.
Vậy transaction thực sự là gì? Nói một cách đơn giản, một transaction là một tập hợp các thao tác hoặc công việc trên dữ liệu mà phải được thực hiện hoàn toàn hoặc không thực hiện gì cả. Hãy tưởng tượng transaction như một thỏa thuận: nếu tất cả các điều kiện của thỏa thuận được đáp ứng, thì thỏa thuận sẽ hoàn tất; nếu không, mọi thứ sẽ trở về trạng thái ban đầu như chưa có gì xảy ra.

Hãy nghĩ về transactions như việc chuyển tiền từ tài khoản ngân hàng này sang tài khoản khác. Nếu quá trình chuyển tiền không hoàn tất vì bất kỳ lý do gì (như mất kết nối mạng), thì số tiền sẽ không bị mất: hoặc tiền sẽ được chuyển hoàn toàn, hoặc không có gì thay đổi. Điều này đảm bảo rằng tài khoản của bạn luôn ở trạng thái chính xác.

Trong các hệ thống lớn, như các trang web thương mại điện tử hay ngân hàng, transactions giúp đảm bảo rằng dữ liệu của bạn luôn an toàn và chính xác.

Transaction state. Nguồn: geeksforgeeks

Các tính chất của một Transaction

Mỗi transaction phải tuân thủ bốn thuộc tính cơ bản, thường được gọi là ACID: Atomicity (Tính nguyên tử), Consistency (Tính nhất quán), Isolation (Tính cô lập), và Durability (Tính bền vững).

Atomicity (Tính nguyên tử): Đảm bảo rằng tất cả các thao tác trong một transaction được thực hiện toàn bộ hoặc không có thao tác nào được thực hiện. Nếu có bất kỳ lỗi nào xảy ra, tất cả các thay đổi sẽ được hoàn nguyên lại về trạng thái ban đầu.

1
2
3
4
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;

Nếu một trong hai câu lệnh UPDATE thất bại, toàn bộ transaction sẽ bị rollback và không có thay đổi nào được áp dụng.

Consistency (Tính nhất quán): Đảm bảo rằng transaction đưa cơ sở dữ liệu từ một trạng thái nhất quán này sang một trạng thái nhất quán khác. Bất kỳ dữ liệu nào được viết vào cơ sở dữ liệu phải tuân theo tất cả các quy tắc xác định trước, bao gồm ràng buộc, triggers, v.v.

1
2
3
4
5
BEGIN;
-- Giả sử có ràng buộc CHECK (balance >= 0) trên cột balance
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;

Nếu sau khi trừ 100 đô la từ Tài khoản 1 mà số dư của tài khoản này dưới 0, transaction sẽ bị rollback để duy trì tính nhất quán.

Isolation (Tính cô lập): Đảm bảo rằng các thao tác của các transaction khác nhau không bị xung đột với nhau. Mỗi transaction sẽ hoạt động như thể nó là transaction duy nhất trong hệ thống. ví dụ

1
2
3
4
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;

Khi sử dụng mức cô lập SERIALIZABLE, PostgreSQL đảm bảo rằng không có transaction nào khác có thể làm thay đổi dữ liệu mà transaction hiện tại đang làm việc cho đến khi nó hoàn thành.

Durability (Tính bền vững): Đảm bảo rằng một khi transaction đã được cam kết, nó sẽ vẫn tồn tại ngay cả khi hệ thống gặp sự cố.

1
2
3
4
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;

Khi câu lệnh COMMIT được thực hiện, PostgreSQL sẽ ghi các thay đổi vào WAL. Nếu hệ thống gặp sự cố ngay sau khi commit, các thay đổi này vẫn sẽ được áp dụng khi hệ thống khởi động lại.

Transaction ACID. Nguồn: https://sqlmct.com/

Tổng kết
Các tính chất ACID được PostgreSQL đảm bảo thông qua các cơ chế logging, constraints, isolation levels, và Write-Ahead Log (WAL). Những cơ chế này đảm bảo rằng các transaction được thực hiện một cách an toàn và hiệu quả, giữ cho dữ liệu luôn nhất quán và bền vững.

Transaction trong Spring

Cách mà plain JDBC Transaction Management vận hành

JDBC(Java Database Connectivity) là một API tiêu chuẩn(standard) của Java, cung cấp một phương thức chung để các ứng dụng Java có thể kết nối và tương tác với cơ sở dữ liệu quan hệ (RDBMS) như MySQL, PostgreSQL, Oracle, SQL Server, v.v. JDBC cho phép các ứng dụng thực hiện các thao tác như gửi câu lệnh SQL, truy vấn dữ liệu, cập nhật cơ sở dữ liệu và xử lý kết quả trả về.

Một plain JDBC transaction sẽ được thể hiện như sau

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import java.sql.Connection;

Connection connection = dataSource.getConnection(); // (1)

try (connection) {
    connection.setAutoCommit(false); // (2)
    // execute some SQL statements...
    connection.commit(); // (3)

} catch (SQLException e) {
    connection.rollback(); // (4)
}

Đây là cách để ‘bắt đầu’ một transaction trong Java qua 4 bước cơ bản như sau:

Bước 1: Lấy một kết nối tới cơ sở dữ liệu từ DataSource.
Bước 2: Tắt tính năng auto-commit để bắt đầu một transaction mới.
Bước 3: Commit transaction sau khi tất cả các thao tác SQL hoàn tất mà không có lỗi xảy ra.
Bước 4: Rollback transaction nếu có lỗi xảy ra, đảm bảo cơ sở dữ liệu quay lại trạng thái ban đầu trước khi transaction bắt đầu.

4 bước trên chính là 4 bước cơ bản mà bất kỳ một @Transaction annotation nào thực hiện khi bạn khai báo nó với hàm excute đi kèm

@Transactional Annotation

Spring Boot cung cấp annotation @Transactional để khai báo một phương thức hoặc một lớp là transactional. Khi một phương thức được đánh dấu bằng @Transactional, Spring sẽ tự động quản lý transaction cho phương thức đó, bao gồm việc bắt đầu, setAutoCommit false, commit, và rollback transaction khi cần thiết. (giống như 4 bước trong đoạn code ở trên)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class AccountService {


    @Transactional
    public int transferMoney(AccoutUser accountUser) {
        // thực hiện vài action để kiểm tra
        // thực hiện việc chuyển tiền
        // trừ tiền vào tài khoản user
       
       return moneyTransfered;
    }
}

Trong ví dụ trên, phương thức transferMoney được đánh dấu với @Transactional. Điều này đảm bảo rằng nếu có bất kỳ lỗi nào xảy ra trong quá trình chuyển tiền, toàn bộ transaction sẽ bị rollback.

Bây giờ, với ví dụ đoạn code về JDBC plain transaction ở trên, hãy cùng đoán xem @Transactional thực sự translate đoạn code thành các step cơ bản như thế nào?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class AccountService {
    @Transactional
    public int transferMoney(AccoutUser accountUser) {
        Connection connection = dataSource.getConnection(); // (1)
        try (connection) {
            connection.setAutoCommit(false); // (1)

        // thực hiện vài action để kiểm tra
        // thực hiện việc chuyển tiền
        // trừ tiền vào tài khoản user (2)

            connection.commit(); // (1)
            return moneyTransfered
        } catch (SQLException e) {
            connection.rollback(); // (1)
        }
    }
}

(1) Đây chỉ là việc mở và đóng kết nối JDBC theo cách tiêu chuẩn. Đó là những gì annotation transaction của Spring thực hiện tự động cho bạn, mà bạn không cần phải viết nó một cách rõ ràng.
(2) Đây là code của bạn, save accountUser qua DAO hoặc một cái gì đó tương tự.

@Transactional under the cover

Spring không thực sự viết lại class Java của bạn, như mình đã làm ở trên, để insert các đoạn code (1) (trừ khi bạn đang sử dụng các kỹ thuật nâng cao như bytecode weaving, nhưng chúng ta sẽ không đề cập tới nó ở đây).

Method transferMoney() của bạn thực sự chỉ execute đoạn code(2), không có cách nào để thay đổi điều đó ngay lập tức.

Nhưng Spring có một benefit. Phần core wrap service của nó là một IoC container. Nó tạo một instance cho class AccountService cho bạn và đảm bảo rằng instance AccountService đó được tự động inject vào bất kỳ bean nào khác cần một AccountService.

How IOC container store and load all beans into your app: https://javaconceptoftheday.com

Bây giờ, mỗi khi bạn sử dụng @Transactional trong một bean, Spring sử dụng một tiny trick. Nó không chỉ tạo một instance AccountService, mà còn tạo một proxy transaction của AccountService đó.

Spring thực hiện điều đó thông qua một method gọi là proxy-through-subclassing với sự trợ giúp của thư viện Cglib. Cũng có các cách khác để tạo proxies (như Dynamic JDK proxies), nhưng chúng ta sẽ không đề cập ở thời điểm này.

Hãy xem proxies hoạt động trong hình này:

Nguồn: https://www.marcobehler.com

Như bạn có thể thấy từ diagram, proxy có một nhiệm vụ.

  1. open/close kết nối database connections/transactions.
  2. Và sau đó chuyển tiếp đến UserService thực sự, cái mà bạn đã viết.
  3. Và các bean khác, như UserRestController của bạn sẽ không bao giờ biết rằng chúng đang nói chuyện với một proxy, chứ không phải đối tượng thực sự.

Vì vậy, hãy cẩn thận khi sử dụng @Transaction bởi vì nếu các bạn call method with @Transaction ở trong chính class có thể Transaction sẽ không được tạo ra bởi vì khi gọi trong chính class thì sẽ không có proxy nào được tạo giữa các bean với nhau cả

Transaction Propagation

Spring Boot cho phép config propagation (kế thừa) của transactions thông qua thuộc tính propagation của @Transactional. Điều này xác định cách một transaction hiện tại sẽ ảnh hưởng đến các transactions bên trong.

Các tùy chọn propagation phổ biến:
REQUIRED: Sử dụng transaction hiện tại hoặc bắt đầu một transaction mới nếu chưa có.
REQUIRES_NEW: Luôn bắt đầu một transaction mới, suspend transaction hiện tại nếu có.
MANDATORY: Sử dụng transaction hiện tại hoặc ném ngoại lệ nếu không có transaction nào đang tồn tại.

1
2
3
4
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void someOtherMethod() {
    // Logic here will always run in a new transaction
}

Transaction Isolation

Spring Boot cho phép cấu hình mức độ isolation của transactions thông qua property isolation của @Transactional. Các mức độ cô lập bao gồm:
DEFAULT: Sử dụng mức độ cô lập mặc định của cơ sở dữ liệu.
READ_UNCOMMITTED: Cho phép đọc các thay đổi chưa được commit.
READ_COMMITTED: Chỉ cho phép đọc các thay đổi đã được commit.
REPEATABLE_READ: Đảm bảo rằng các đọc lặp lại trong cùng một transaction sẽ thấy cùng một dữ liệu.
SERIALIZABLE: Đảm bảo mức độ cô lập cao nhất bằng cách thực hiện các transactions một cách tuần tự.

1
2
3
4
@Transactional(isolation = Isolation.SERIALIZABLE)
public void isolatedMethod() {
    // Logic here runs with serializable isolation level
}

Transaction Rollback Rules

Spring Boot cho phép config các quy tắc rollback cho transactions thông qua property rollbackFor và noRollbackFor của @Transactional. Điều này xác định các exception sẽ kích hoạt rollback hoặc không rollback.

1
2
3
4
@Transactional(rollbackFor = Exception.class)
public void methodWithRollback() {
    // Logic here will be rolled back if any exception is thrown
}

Tổng Kết

Spring Boot quản lý transactions thông qua việc sử dụng annotation @Transactional, cho phép lập trình viên dễ dàng kiểm soát các thao tác transaction. Các tính năng như propagation, isolation levels, và rollback rules giúp đảm bảo rằng transactions được quản lý một cách hiệu quả và an toàn. Việc sử dụng Spring Boot để quản lý transactions giúp đơn giản hóa quá trình phát triển và duy trì ứng dụng, đồng thời đảm bảo tính toàn vẹn và nhất quán của dữ liệu.
Tuy nhiên, mọi người cũng nên cẩn thận khi dùng @Transaction trong chính class/service của mình vì có thể không đạt được kết quả mong muốn (như mình đã đề cập trong mục 3.3)

Bài viết cũng đã khá dài rồi nên mình xin phép dừng lại tại đây.
Nếu các bạn yêu thích bài viết này và muốn tìm hiểu sâu hơn về các kỹ thuật nâng cao như Distributed Transactions Handling thì hãy comment cho mình biết nhé!

HAPPY CODING!

Nguồn các trích dẫn mình dùng trong blog:
https://www.marcobehler.com/guides/spring-transaction-management-transactional-in-depth
https://www.geeksforgeeks.org/acid-properties-in-dbms
https://www.geeksforgeeks.org/transaction-management

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy