Spring Entity and Transaction

July 18, 2026

1. Entity & Persistence Context

Entity là gì?

Entity là một Java Class đại diện cho một bảng (table) trong Database. Mỗi instance (đối tượng) của Class này tương ứng với một dòng (row) trong bảng.

  • Khai báo bằng annotation @Entity.
  • Bắt buộc phải có một trường làm Primary Key được đánh dấu bằng @Id.

Persistence Context (Ngữ cảnh lưu trữ) là gì?

Persistence Context là một vùng nhớ đệm (First-level Cache / L1 Cache) do JPA EntityManager quản lý trong suốt một Transaction.

  • Nhiệm vụ: Quản lý các Entity instance, theo dõi các thay đổi của chúng (Dirty Checking), và đồng bộ dữ liệu xuống DB khi Transaction commit hoặc khi thực hiện flush().
  • Phạm vi (Scope): Mặc định gắn liền với một EntityManager / Một Transaction (Transaction-scoped).

2. Entity States

Trong vòng đời của mình, một Entity có thể nằm ở 1 trong 4 trạng thái sau đối với Persistence Context:

Entity States

2.1. Transient (Tạm thời)

  • Đặc điểm: Entity vừa được tạo bằng từ khóa new trong Java, chưa có @Id (hoặc id chưa gắn với DB) và chưa được quản lý bởi Persistence Context.
  • Database: Chưa có bản ghi tương ứng dưới DB.
  • Ví dụ: User user = new User("Alice");

2.2. Persistent (Đang được quản lý)

  • Đặc điểm: Entity đang được Persistence Context quản lý và có định danh ID hợp lệ dưới DB.
  • Đặc tính Dirty Checking: Bất kỳ thay đổi nào trên các setter của Entity này sẽ tự động được update xuống DB khi Transaction commit mà không cần gọi repository.save() hay merge().
  • Ví dụ: Lấy ra từ DB bằng repository.findById(1L) hoặc sau khi gọi entityManager.persist(user).

2.3. Detached (Tách rời)

  • Đặc điểm: Entity đã từng ở trạng thái Persistent (có ID trong DB), nhưng hiện tại không còn nằm trong/được quản lý bởi Persistence Context nữa (do Transaction đã kết thúc, hoặc do gọi clear(), detach()).
  • Hệ quả: Các thay đổi trên đối tượng này sẽ không tự động cập nhật xuống DB. Muốn cập nhật phải đưa nó quay lại trạng thái Persistent bằng entityManager.merge().

2.4. Removed (Đã xóa)

  • Đặc điểm: Entity đang được Persistence Context quản lý nhưng đã được đánh dấu để xóa (bằng cách gọi repository.delete() hoặc entityManager.remove()).
  • Database: Bản ghi vẫn còn trong DB cho tới khi Transaction thực hiện flush() hoặc commit().

3. persist() vs save(), merge() vs saveOrUpdate()

persist()

  • JPA Standard, jakarta.persistence.EntityManager (JPA API).
  • void (không giá trị trả về).
  • Quăng lỗi PersistentObjectException nếu truyền vào một Detached Entity.
  • Thời điểm thực thi SQL Chỉ đưa Entity vào Persistence Context. Lệnh INSERT sẽ hoãn lại cho tới khi flush() hoặc commit().

save()

  • org.hibernate.SessionCrudRepository (Hibernate Native / Spring Data)
  • Trả về Entity instance (có thể là một instance khác).
  • Deteched entity: ✅ Thực hiện câu lệnh UPDATE (hoặc INSERT một bản ghi mới tùy thuộc thiết lập ID).
  • Thời điểm thực thi SQL: Sinh ra ID và có thể thực thi lệnh INSERT ngay lập tức để lấy ID trả về.

Tìm hiểu sâu vào code thì Spring Data JPA CrudRepository.save() gồm: persist() và merge()

// Đoạn code mã nguồn đơn giản hóa của SimpleJpaRepository trong Spring Data JPA
@Transactional
public <S extends T> S save(S entity) {
    if (entityInformation.isNew(entity)) {
        // Nếu là Entity mới -> Gọi persist() chuẩn JPA
        entityManager.persist(entity);
        return entity;
    } else {
        // Nếu là Entity cũ/Detached -> Gọi merge() chuẩn JPA
        return entityManager.merge(entity);
    }
}

So sánh tương tự merge() vs saveOrUpdate().

  • merge() (JPA Standard): Copy dữ liệu từ Detached Entity sang một Persistent Instance mới và trả về Instance mới đó. Entity truyền vào ban đầu vẫn giữ nguyên trạng thái Detached. Do copy nên trùng ID trong Persistence Context sẽ UPDATE.
  • saveOrUpdate() (Hibernate Native): Chuyển chính Entity truyền vào thành Persistent Instance.
  • Tính an toàn: merge() an toàn hơn vì giải quyết được xung đột khi đã có một Object cùng ID nằm sẵn trong Session, tránh bị lỗi NonUniqueObjectExceptionsaveOrUpdate() thường gặp.

4. Lazy Loading vs. Eager Loading

Đây là hai chiến lược tải dữ liệu quan hệ (Relationships: @OneToMany, @ManyToOne, v.v.).

Lazy Loading vs. Eager Loading

@Entity
public class User {
    @Id
    private Long id;

    // Default là LAZY
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) 
    private List<Order> orders;
}
  • Khi gọi User user = userRepository.findById(1L).get();
    • Hibernate chỉ chạy 1 câu SQL: SELECT * FROM users WHERE id = 1;
    • Trường orders lúc này chỉ chứa một Hibernate Proxy.
  • Khi gọi user.getOrders().size();
    • Hibernate mới kích hoạt câu SQL thứ 2: SELECT * FROM orders WHERE user_id = 1; để lấy danh sách đơn hàng.
  • Tuy nhiên, việc query bổ sung này bắt buộc phải diễn ra bên trong một Persistence Context / Transaction còn sống.

4.1. Nguyên nhân gây lỗi: LazyInitializationException

// 1. Service Layer
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    // Không có @Transactional hoặc Transaction kết thúc ngay khi thoát hàm
    public User getUser(Long id) {
        return userRepository.findById(id).orElseThrow(); 
        // Session đóng ngay tại đây sau khi tìm xong User!
    }
}

// 2. Controller Layer
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users/{id}")
    public UserResponse getUser(@PathVariable Long id) {
        User user = userService.getUser(id);
        
        // 💥 BÁO LỖI NGAY TẠI ĐÂY!
        // Session đã đóng ở Service, nhưng Controller cố gọi getter của LAZY collection
        int totalOrders = user.getOrders().size(); 
        return new UserResponse(user.getName(), totalOrders);
    }
}

Thông điệp lỗi điển hình: org.hibernate.LazyInitializationException: could not initialize proxy [com.example.User#1] - no Session

4.2. Các Pattern chuẩn để giải quyết (Best Practices)

Để giải quyết triệt để, giải pháp chuẩn nhất là Fetch dữ liệu quan hệ ngay từ câu truy vấn ban đầu ở tầng Repository khi biết chắc chắn sẽ cần dùng tới dữ liệu đó.

Pattern 1: Dùng JOIN FETCH trong @Query (Phổ biến & Linh hoạt)

Cho phép bạn override chiến lược LAZY của field bằng cách chỉ định load kèm dữ liệu ngay trong câu JPQL.

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@Param("id") Long id);
}

JOIN FETCH

Ưu điểm: Chủ động, chính xác, Hibernate sẽ sinh ra 1 câu INNER JOIN hoặc LEFT JOIN để lấy cả User lẫn Order trong 1 query duy nhất.
Notes:

  • ❌ Không dùng cho DTO Projection (vì FETCH chỉ đi kèm với Entity).
  • ❌ Không dùng FETCH JOIN với Pagination (Pageable, PageRequest, setFirstResult, setMaxResults) trên quan hệ 1-N (@OneToMany), Spring Data JPA/Hibernate vẫn cho phép chạy, nhưng đây là một cạm bẫy hiệu năng vô cùng nguy hiểm.

Pattern 2: Dùng @EntityGraph của Spring Data JPA

Đây là giải pháp khai báo (declarative) giúp tránh phải viết thủ công các câu JPQL dài.

public interface UserRepository extends JpaRepository<User, Long> {

    // Tự động JOIN FETCH thuộc tính 'orders' khi query
    @EntityGraph(attributePaths = {"orders"})
    Optional<User> findById(Long id);
}

Ưu điểm: Code gọn gàng, tái sử dụng tốt với các hàm CRUD mặc định của Spring Data JPA.

Pattern 3: Dùng DTO Projection (Tốt nhất cho Read-Only APIs)

Thay vì trả về toàn bộ Managed Entity rồi bị vướng vào các mối quan hệ LAZY/EAGER, bạn query thẳng dữ liệu ra DTO ngay ở DB level.

// 1. Định nghĩa DTO Projection
public record UserOrderSummaryDto(String userName, int orderCount) {}

// 2. Repository query trực tiếp ra DTO
public interface UserRepository extends JpaRepository<User, Long> {

    @Query("""
        SELECT new com.example.UserOrderSummaryDto(u.name, SIZE(u.orders)) 
        FROM User u WHERE u.id = :id
    """)
    Optional<UserOrderSummaryDto> getUserSummary(@Param("id") Long id);
}

Ưu điểm: Tối ưu hiệu năng vượt trội, không tốn tài nguyên quản lý Entity State trong Persistence Context, triệt tiêu 100% rủi ro LazyInitializationException.

Pattern 4: Mở rộng Transaction Boundary với @Transactional

Nếu logic xử lý thực sự nằm trong Service, hãy đảm bảo hàm gọi getter của Lazy Collection nằm bên trong phạm vi @Transactional.

@Service
public class UserService {

    @Transactional(readOnly = true) // Giữ Session mở trong suốt hàm này
    public UserDto getUserWithOrders(Long id) {
        User user = userRepository.findById(id).orElseThrow();
        
        // Hoạt động an toàn vì Session vẫn đang OPEN
        List<OrderDto> orderDtos = user.getOrders().stream()
                .map(OrderDto::fromEntity)
                .toList();

        return new UserDto(user.getName(), orderDtos);
    }
}

4.3. Warning

  • ❌ Đổi sang FetchType.EAGER hàng loạt:
    • Tác hại: Tải dữ liệu thừa không cần thiết, làm chậm toàn bộ các API khác truy cập vào Entity đó.

5. Architectural Comparison: L1 vs. L2 Cache

Persistence Context (L1 Cache) operates at the EntityManager / Session level (bound to a single transaction), the L2 (Second-Level) Cache operates at the SessionFactory / Application level.

While L1 Cache is enabled by default and short-lived, L2 Cache is optional, shared across all sessions, and survives beyond the scope of a single transaction.

L1 L2 Cache

Cache Details

L1 Cache is transaction-scoped and mandatory, whereas L2 Cache is application-scoped, optional, and shared across all sessions. L2 cache prevents redundant database hits across different user requests for read-heavy, rarely-changing data. To use it, we configure a provider (like Ehcache or Redis), mark entities with @Cacheable, and choose an appropriate CacheConcurrencyStrategy (like READ_WRITE or READ_ONLY).

5. The 6 Classic Cases Where @Transactional Fails or Doesn't Roll Back

5.1. Catching the Exception Inside a try-catch Block (Most Common)

Spring’s transaction manager only knows an error happened if an exception escapes the @Transactional method. If you swallow the exception in a try-catch block, Spring assumes everything went smoothly and commits the transaction.

@Transactional
public void processOrder(OrderDto dto) {
    orderRepository.save(new Order(...));
    
    try {
        paymentService.charge(dto); // Throws RuntimeException
    } catch (Exception e) {
        log.error("Payment failed", e); 
        // ❌ Spring never sees the exception! The Order is committed anyway.
    }
}

Fix: Re-throw the exception, or manually mark the transaction for rollback using TransactionAspectSupport.currentTransactionStatus().setRollbackOnly().

The cleanest and most standard way is to either re-throw the original exception or wrap it in a custom RuntimeException. Once the exception escapes the @Transactional boundary, Spring detects it and triggers a rollback.

@Service
public class OrderService {

    @Autowired private OrderRepository orderRepository;
    @Autowired private PaymentClient paymentClient;

    @Transactional(rollbackFor = Exception.class) // Good practice to include rollbackFor
    public void createOrder(OrderDto dto) {
        Order order = orderRepository.save(new Order(dto));

        try {
            paymentClient.charge(dto.getAmount());
        } catch (Exception e) {
            log.error("Payment failed for order: {}", dto.getId(), e);
            
            // ✅ FIX: Re-throw as a RuntimeException (or your domain exception)
            throw new PaymentFailedException("Could not complete payment: " + e.getMessage(), e);
        }
    }
}

Why it works: The exception escapes createOrder(). Spring’s transaction interceptor catches it, marks the transaction for rollback, and then propagates the exception up to the caller or @ExceptionHandler.

Fix 2: Manually mark for Rollback using setRollbackOnly()

If your method must return a response or complete execution without throwing an exception back to the caller, you can explicitly instruct Spring to roll back the current transaction using TransactionAspectSupport.

@Service
public class OrderService {

    @Autowired private OrderRepository orderRepository;
    @Autowired private PaymentClient paymentClient;

    @Transactional
    public OrderResultDto createOrder(OrderDto dto) {
        Order order = orderRepository.save(new Order(dto));

        try {
            paymentClient.charge(dto.getAmount());
            return new OrderResultDto(true, "Order placed successfully");
            
        } catch (Exception e) {
            log.error("Payment failed for order: {}", dto.getId(), e);

            // ✅ FIX: Manually mark the current transaction status as rollback-only
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

            // Now you can safely return a DTO/Status without throwing an exception
            return new OrderResultDto(false, "Payment failed, order rolled back");
        }
    }
}

5.2. Throwing a Checked Exception (Default Rollback Behavior)

By default, Spring @Transactional ONLY rolls back on Unchecked Exceptions (RuntimeException and Error). It DOES NOT roll back on Checked Exceptions (e.g., IOException, SQLException, or custom exceptions extending Exception).

@Transactional // ❌ Will NOT rollback on Checked Exception by default!
public void updateUser() throws CustomCheckedException {
    userRepository.save(user);
    throw new CustomCheckedException("Something failed");
}

Fix: Explicitly configure the rollback rule:

@Transactional(rollbackFor = Exception.class)

5.3. Self-Invocation (Calling a @Transactional method within the same class)

Spring uses Dynamic Proxies (AOP) to wrap @Transactional beans. The proxy intercepts the call, starts a transaction, and then calls your actual method.

If Method A (non-transactional or transactional) calls Method B (@Transactional) inside the same class, the call bypasses the Spring Proxy (this.methodB()), so no transaction boundary is created.

@Service
public class UserService {

    public void registerUser(User user) { // Called from outside
        // ❌ Called directly via 'this', bypassing the AOP Proxy!
        this.saveData(user); 
    }

    @Transactional
    public void saveData(User user) {
        userRepository.save(user);
        throw new RuntimeException(); // Transactional annotation is IGNORED!
    }
}

Fix: Move saveData() to a separate service/component, or inject UserService into itself (lazy), or use programmatic transaction management (TransactionTemplate).

Fix 1: Move to a Separate Service / Component (Best Practice)

The cleanest architectural approach is to follow the Single Responsibility Principle. Move the transactional method into a separate bean so the call naturally goes through Spring's AOP proxy.

// 1. Separate component dedicated to database operations
@Component
public class UserDataHandler {

    @Autowired private UserRepository userRepository;

    @Transactional // ✅ Works! Called from another bean through the Spring Proxy
    public void saveData(User user) {
        userRepository.save(user);
        throw new RuntimeException("DB error!"); // Will trigger rollback!
    }
}

// 2. Main Service
@Service
public class UserService {

    @Autowired private UserDataHandler userDataHandler;

    public void registerUser(User user) {
        // ... validation logic ...
        
        // ✅ Call goes through the Proxy of UserDataHandler
        userDataHandler.saveData(user);
    }
}

Fix 2: Self-Injection with @Lazy

If moving the method to a new class creates too much boilerplate or violates your project structure, you can inject the UserService bean into itself. By adding @Lazy, Spring breaks the circular dependency loop and provides a reference to the Proxy instance instead of this.

@Service
public class UserService {

    @Autowired private UserRepository userRepository;
    
    // ✅ Inject the Spring Proxy of THIS service
    @Autowired @Lazy 
    private UserService self;

    public void registerUser(User user) {
        // ... validation logic ...

        // ✅ Call the method ON THE PROXY (self), not on 'this'!
        self.saveData(user); 
    }

    @Transactional // ✅ Works! Invoked via the 'self' proxy
    public void saveData(User user) {
        userRepository.save(user);
        throw new RuntimeException("DB error!"); // Will trigger rollback!
    }
}

Fix 3: Use Programmatic Transactions (TransactionTemplate)

When you need granular control over transaction boundaries without creating extra methods/components or relying on AOP magic, use Spring's TransactionTemplate.

@Service
public class UserService {

    @Autowired private UserRepository userRepository;
    @Autowired private TransactionTemplate transactionTemplate;

    public void registerUser(User user) {
        // Non-transactional validation logic
        validateUser(user);

        // ✅ Wrap ONLY the database operation inside a TransactionTemplate callback
        transactionTemplate.executeWithoutResult(status -> {
            try {
                userRepository.save(user);
                // Any exception here automatically marks status.setRollbackOnly()
            } catch (Exception e) {
                status.setRollbackOnly(); // Manual rollback if needed
                throw e;
            }
        });
    }

    private void validateUser(User user) {
        // Validation code
    }
}

5.4. Method is private, protected, or package-private

Spring AOP proxies wrap public methods. By default, @Transactional applied to private or protected methods is silently ignored without throwing an error.

@Service
public class OrderService {

    @Transactional // ❌ IGNORED because it's private!
    private void internalSave() {
        orderRepository.save(new Order());
    }
}

Fix: Make the method public.

5.5. Catching Exceptions in Asynchronous or Reactive Threads (@Async)

If your @Transactional method spins up a new thread (e.g., using @Async or CompletableFuture), the new thread runs on a different thread context. Spring's TransactionSynchronizationManager binds transactions to the ThreadLocal of the current thread.

@Transactional
public void process() {
    userRepository.save(user); // Running in Main Thread (Transaction A)

    CompletableFuture.runAsync(() -> {
        // ❌ Running in a separate thread! 
        // Has NO access to Transaction A, and exceptions thrown here won't rollback Transaction A.
        auditRepository.save(audit);
    });
}

If you still want to execute @Async in a separate independent transaction

  • @EnableAsync
  • @Async on a method, only use for @Service or @Component.
  • @Transactional(propagation = Propagation.REQUIRES_NEW)

Audit Service

@Service
public class AuditService {

    @Autowired 
    private AuditLogRepository auditLogRepository;

    @Async // 1. Executes in a new thread pool thread
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 2. Forces a new, independent transaction
    public void saveAuditAsync(String action, Long userId, String details) {
        AuditLog log = new AuditLog(action, userId, details, LocalDateTime.now());
        
        // This transaction is completely isolated from the caller thread's transaction
        auditLogRepository.save(log);
    }
}

Order Service

@Service
public class OrderService {

    @Autowired private OrderRepository orderRepository;
    @Autowired private AuditService auditService;

    @Transactional // Main Transaction A
    public void processOrder(OrderDto dto) {
        // 1. Save business data (Transaction A)
        Order order = orderRepository.save(new Order(dto));

        // 2. Trigger Audit (Dispatched to @Async thread -> Starts Transaction B)
        auditService.saveAuditAsync("ORDER_CREATED", dto.getUserId(), "Order #" + order.getId());

        // Even if Transaction A rolls back due to a downstream error, 
        // Transaction B (Audit) on the async thread runs and commits independently!
    }
}

5.6. Using Non-Transactional Storage Engines (e.g., MySQL MyISAM)

If your underlying database table uses a storage engine that does not support transactions (like old MySQL MyISAM instead of InnoDB), Spring will execute the transaction lifecycle code, but the database will simply ignore the rollback command.

6. In Spring Framework, @Transactional has got 7 Propagation

Mặc định (Default), giá trị này là Propagation.REQUIRED.

  • REQUIRED (default): Dùng chung hoặc Tạo mới. Nếu đã có transaction (từ caller), dùng lại transaction đó. Nếu chưa có, tạo một transaction mới.

  • REQUIRES_NEW: Luôn tạo mới. Luôn luôn khởi tạo một transaction hoàn toàn độc lập. Nếu đang có transaction cũ, transaction cũ sẽ bị tạm dừng (suspended) cho đến khi transaction mới chạy xong.

  • NESTED: Transaction lồng nhau. Nếu đã có transaction, tạo một Savepoint. Nếu B bị lỗi, nó rollback về Savepoint đó chứ không ảnh hưởng đến A. (Chỉ dùng được với JDBC/JPA hỗ trợ Savepoint).

  • SUPPORTS: Có thì dùng, không thì thôi. Nếu caller có transaction thì chạy trong transaction đó. Nếu caller không có, chạy bình thường theo kiểu phi-transaction (non-transactional).

  • NOT_SUPPORTED: Luôn chạy non-transaction. Nếu có transaction đang chạy, tạm dừng transaction đó lại và thực thi phương thức theo kiểu non-transactional.

  • MANDATORY: Bắt buộc phải có sẵn. Phải được gọi từ một transaction đã mở từ trước. Nếu caller không có transaction, Spring sẽ quăng IllegalTransactionStateException.

  • NEVER: Cấm transaction. Nếu được gọi bên trong một transaction, Spring sẽ quăng IllegalTransactionStateException.

7. Transaction Isolation.

Mục đích chính của Isolation là cân bằng giữa tính toàn vẹn dữ liệu (Consistency)hiệu năng hệ thống (Performance).

7.1. Các hiện tượng tranh chấp dữ liệu (Concurrency Side Effects)

Để hiểu các cấp độ Isolation, trước hết cần biết 3 hiện tượng "xấu" có thể xảy ra khi nhiều Transaction chạy song song:

  • Dirty Read (Đọc rác): Transaction A đọc dữ liệu mà Transaction B đang sửa nhưng chưa Commit. Nếu B bị Rollback, dữ liệu A vừa đọc là hoàn toàn hư cấu.

  • Non-Repeatable Read (Đọc không lặp lại): Transaction A đọc một dòng dữ liệu 2 lần trong cùng 1 transaction, nhưng thu được 2 kết quả khác nhau vì Transaction B đã UPDATE hoặc DELETE dòng đó và Commit ở giữa 2 lần đọc của A.

  • Phantom Read (Đọc bóng ma): Transaction A thực hiện một truy vấn tập hợp (VD: COUNT(*) các user có age > 20) 2 lần, nhưng số lượng trả về khác nhau vì Transaction B mới INSERT thêm dòng mới thỏa điều kiện và Commit ở giữa.

7.2. 4 Cấp độ Transaction Isolation chuẩn ANSI/ISO SQL

Các Isolation level được sắp xếp từ lỏng lẻo nhất (hiệu năng cao nhất) đến chặt chẽ nhất (hiệu năng thấp nhất):

1. READ_UNCOMMITTED

  • Cách hoạt động: Cho phép đọc dữ liệu chưa được Commit từ Transaction khác.

  • Hiện tượng gặp phải: Có thể bị Dirty Read, Non-Repeatable Read, và Phantom Read.

  • Thực tế: Hầu như rất ít khi dùng trong ứng dụng thực tế vì quá rủi ro dữ liệu sai lệch.

2. READ_COMMITTED (Mặc định của PostgreSQL, Oracle, SQL Server)

  • Cách hoạt động: Chỉ cho phép đọc những dữ liệu đã được Commit.

  • Hiện tượng gặp phải: Giải quyết được Dirty Read. Tuy nhiên vẫn có thể bị Non-Repeatable ReadPhantom Read.

  • Thực tế: Là cấp độ phổ biến và tối ưu nhất cho phần lớn các ứng dụng web thông thường.

3. REPEATABLE_READ (Mặc định của MySQL InnoDB)

  • Cách hoạt động: Đảm bảo khi Transaction A đọc một dòng dữ liệu, bất kỳ lần đọc nào sau đó trong cùng Transaction A vẫn sẽ thấy đúng giá trị đó (dù B có UPDATECOMMIT thì A vẫn nhìn thấy snapshot cũ).

  • Hiện tượng gặp phải: Giải quyết được Dirty Read và Non-Repeatable Read. Nhưng vẫn có thể dính Phantom Read (Lưu ý: MySQL InnoDB sử dụng cơ chế MVCC & Next-Key Locks nên hạn chế được cả Phantom Read ở level này).

4. SERIALIZABLE

  • Cách hoạt động: Các Transaction được thực thi tuần tự nối đuôi nhau (Lock hoàn toàn bảng/dòng dữ liệu liên quan).

  • Hiện tượng gặp phải: Chống lại TẤT CẢ các lỗi (Không Dirty Read, Không Non-Repeatable Read, Không Phantom Read).

  • Thực tế: Rất an toàn nhưng hiệu năng cực kỳ kém do Deadlock và nghẽn hàng đợi (queue). Chỉ dùng cho các tác vụ tài chính nhạy cảm tuyệt đối.

7.3. Cách cấu hình Isolation trong Spring Framework

Bạn có thể dễ dàng thiết lập Isolation level cho từng phương thức thông qua thuộc tính isolation của @Transactional:

@Service
public class BankAccountService {

    // Sử dụng READ_COMMITTED
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void transferMoney(...) { ... }

    // Sử dụng SERIALIZABLE cho tác vụ cực kỳ quan trọng
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void executeCriticalFinancialReport(...) { ... }
}

Lưu ý quan trọng:

Giá trị mặc định Isolation.DEFAULT trong Spring có nghĩa là sử dụng Isolation level mặc định của Database bên dưới (Chẳng hạn nếu dùng MySQL InnoDB sẽ là REPEATABLE_READ, còn nếu dùng PostgreSQL sẽ là READ_COMMITTED).