Home Engineering

Tất cả về Transaction trong Java: Từ ACID đến Distributed Systems

07 June 2026 · 38 min read
Table of contents

Transaction là thứ bạn không thấy khi nó hoạt động đúng, và không thể giải thích khi nó sai. Một bug transaction trong hệ thống thanh toán có thể khiến tiền biến mất, xuất hiện hai lần, hoặc trạng thái order và payment không đồng nhất mà không có exception nào được throw. Phần lớn lỗi không đến từ thiếu annotation @Transactional, mà đến từ không hiểu annotation đó thực sự làm gì, khi nào nó không hoạt động, và tại sao.

Bài này giải thích transaction từ lý do tồn tại, không từ API. Khi hiểu tại sao database cần transaction, tại sao ACID được thiết kế như vậy, tại sao Spring chọn proxy-based approach — thì mọi pitfall đều trở nên hiển nhiên thay vì bí ẩn.


Phần 1: Tại Sao Transaction Tồn Tại

Vấn đề gốc: Concurrent access và partial failure

Hãy bắt đầu từ thứ đơn giản nhất: chuyển tiền giữa hai tài khoản.

-- Chuyển 500k từ account A sang account B
UPDATE accounts SET balance = balance - 500000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 500000 WHERE id = 'B';

Hai câu lệnh SQL riêng biệt. Điều gì xảy ra nếu:

  • Server crash sau câu lệnh đầu tiên? → Account A mất 500k, account B không nhận được gì.
  • Câu lệnh thứ hai fail do constraint violation? → Tương tự.
  • Hai user đồng thời đọc balance của A, cả hai thấy đủ tiền, cả hai chuyển? → A bị trừ tiền hai lần.
User 1: đọc A.balance = 1,000,000         |
                                            |
User 2: đọc A.balance = 1,000,000         |
                                            |
User 1: ghi A.balance = 500,000            |  ← trừ 500k
                                            |
User 2: ghi A.balance = 500,000            |  ← trừ thêm 500k nhưng đọc từ 1,000,000!
                                            |
Thực tế: A.balance = 500,000               |  (đáng lẽ phải là 0 nếu cả hai hợp lệ)
          hoặc A.balance = -500,000         |  (nếu không có check)

Đây là lost update — một trong nhiều race condition mà transaction giải quyết.

Tại sao “chỉ execute SQL” không đủ

Trước khi transaction tồn tại (và ngày nay ở những hệ thống thiết kế sai), người ta dùng application-level locking: đặt flag trong database, check flag trước khi write, clear flag sau. Vấn đề:

  1. Flag không atomic với write: check và write là hai operations riêng, vẫn có race condition.
  2. Không handle crash: nếu app crash sau khi set flag nhưng trước khi clear, flag bị stuck vĩnh viễn.
  3. Không compose: hai đoạn code độc lập không biết nhau đang dùng cùng resource.
  4. Không portable: mỗi team implement locking khác nhau.

Transaction giải quyết tất cả ở database layer — nơi có đủ thông tin và control.

E-commerce order creation — failure modes thực tế

// Không có transaction — code thực tế từ production legacy system:
public void placeOrder(OrderRequest request) {
    // Step 1: Create order
    long orderId = orderDao.insert(request);

    // Step 2: Deduct inventory
    for (OrderItem item : request.getItems()) {
        inventoryDao.deduct(item.getProductId(), item.getQuantity());
        // Nếu deduct lần 3 fail (hết inventory) → order đã tạo, 2 items đã deduct
        // Database ở trạng thái không nhất quán
    }

    // Step 3: Create payment record
    paymentDao.createPending(orderId, request.getTotalAmount());
    // Nếu fail ở đây → order tạo, inventory deducted, không có payment record
}

Mỗi step fail tạo ra trạng thái khác nhau. Không có cách biết system ở trạng thái nào sau failure. Đây là lý do transaction tồn tại: group nhiều operations thành một unit — hoặc tất cả succeed, hoặc tất cả được rollback về trạng thái ban đầu.


Phần 2: Hiểu ACID Đúng Cách

Atomicity — “All or nothing”

Tại sao tồn tại: Partial updates là trạng thái không nhất quán không thể recover tự động.

Cách database implement: Write-Ahead Log (WAL). Trước khi thay đổi data trên disk, PostgreSQL ghi log entry vào WAL buffer. WAL flush xuống disk trước khi transaction commit. Khi crash:

Transaction đang chạy → crash → restart
PostgreSQL đọc WAL
Nếu thấy BEGIN nhưng không thấy COMMIT → undo all changes từ transaction đó
Nếu thấy COMMIT → redo nếu data chưa flush xuống disk
WAL entries:
[BEGIN txn=1234]
[UPDATE accounts SET balance=500000 WHERE id='A', old_val=1000000]
[UPDATE accounts SET balance=1500000 WHERE id='B', old_val=1000000]
[COMMIT txn=1234]

Crash xảy ra sau COMMIT entry → recovery redo cả hai UPDATE
Crash xảy ra trước COMMIT entry → recovery undo cả hai UPDATE

Misconception phổ biến: Atomicity không đảm bảo performance. Một transaction với 10,000 UPDATE statements vẫn là atomic — tất cả 10,000 hoặc không cái nào.

Consistency — Database không vi phạm rules của nó

Tại sao tồn tại: Constraints (NOT NULL, FOREIGN KEY, CHECK) chỉ có nghĩa nếu chúng luôn đúng, không chỉ đúng “thường”.

Misconception quan trọng: Consistency trong ACID là consistency của database, không phải consistency trong hệ thống phân tán. Nó có nghĩa là database rules (constraints, triggers) không bị vi phạm sau transaction. Đây là property yếu nhất trong ACID — application code có thể tạo ra consistent data theo database nhưng inconsistent theo business logic.

-- Database "consistent" nhưng business logic sai:
BEGIN;
UPDATE accounts SET balance = -500000 WHERE id = 'A'; -- Hợp lệ nếu không có CHECK constraint
COMMIT;
-- Database OK (không vi phạm constraint nào), nhưng balance âm là sai về business

Isolation — Concurrent transactions không thấy nhau

Tại sao tồn tại: Nhiều transaction chạy đồng thời cần hành xử như thể chúng chạy tuần tự. Không có isolation, mọi race condition đều có thể xảy ra.

Cách PostgreSQL implement: MVCC (Multi-Version Concurrency Control). Thay vì lock data khi đọc, PostgreSQL giữ nhiều version của mỗi row. Mỗi transaction thấy snapshot của database tại thời điểm nó bắt đầu (hoặc statement bắt đầu, tùy isolation level).

Row "account A" trong MVCC:
Version 1: balance=1,000,000 (created_by=txn_100, deleted_by=txn_200)
Version 2: balance=500,000   (created_by=txn_200, deleted_by=null)

Transaction txn_300 bắt đầu khi txn_200 đang chạy:
→ txn_300 thấy Version 1 (txn_200 chưa commit)
→ txn_300 đọc balance=1,000,000

Transaction txn_400 bắt đầu sau khi txn_200 commit:
→ txn_400 thấy Version 2
→ txn_400 đọc balance=500,000

Durability — Committed data không mất

Tại sao tồn tại: RAM là volatile. Sau khi nói “transaction committed” với user, database phải đảm bảo data tồn tại dù crash.

Cách implement: WAL flush-to-disk trước khi commit return. fsync() system call đảm bảo OS không buffer write. Đây là lý do commit chậm hơn write thông thường — phải đợi disk I/O.

Trade-off: synchronous_commit = off trong PostgreSQL tăng throughput nhưng có thể mất ~1–2 giây data sau crash. Chấp nhận được cho session logs, không chấp nhận cho financial data.

Transaction Lifecycle — Từ BEGIN đến COMMIT

Application          PostgreSQL
    │                    │
    │── BEGIN ───────────>│  PostgreSQL gán transaction ID (XID)
    │                    │  Snapshot được tạo (tùy isolation level)
    │                    │
    │── UPDATE ──────────>│  Change ghi vào shared buffer (memory)
    │                    │  WAL entry ghi vào WAL buffer
    │                    │  Lock acquired trên affected rows
    │                    │
    │── SELECT ──────────>│  Đọc từ snapshot, không thấy uncommitted changes
    │                    │
    │── COMMIT ──────────>│  WAL buffer flush xuống disk (fsync)
    │                    │  Locks released
    │                    │  XID marked as committed trong pg_xact
    │<── OK ─────────────│
    │                    │
    │                    │  [Background] VACUUM cleanup old versions

Phần 3: Concurrency Problems Transactions Giải Quyết

Lost Update

Hai transaction đọc cùng một value, cả hai modify, cả hai write lại — một write bị overwrite.

Time    T1 (User 1 vote)          T2 (User 2 vote)          DB
 1      READ votes = 100
 2                                READ votes = 100
 3      WRITE votes = 101
 4                                WRITE votes = 101          ← T1's update lost!
 5                                COMMIT

Result: votes = 101 (đáng lẽ phải là 102)
-- Reproduce trong PostgreSQL (isolation: READ COMMITTED)
-- Session 1:
BEGIN;
SELECT vote_count FROM posts WHERE id = 1; -- 100
-- (pause)
UPDATE posts SET vote_count = 101 WHERE id = 1;
COMMIT;

-- Session 2 (chạy đồng thời):
BEGIN;
SELECT vote_count FROM posts WHERE id = 1; -- 100
UPDATE posts SET vote_count = 101 WHERE id = 1; -- Overwrites Session 1!
COMMIT;

Fix: Atomic update (không cần đọc trước), hoặc optimistic locking, hoặc SELECT FOR UPDATE.

-- Atomic update — không có race condition
UPDATE posts SET vote_count = vote_count + 1 WHERE id = 1;

Dirty Read

Transaction đọc data đã được write bởi transaction chưa commit. Nếu transaction kia rollback — data bạn đọc không bao giờ tồn tại.

Time    T1 (Payment processing)    T2 (Fraud check)          DB
 1      BEGIN
 2      UPDATE payment SET status='PROCESSING'
 3                                 BEGIN
 4                                 READ payment status = 'PROCESSING'  ← Dirty read!
 5                                 (decides: not fraud, allow)
 6      -- payment gateway fails
 7      ROLLBACK
 8                                 (fraud check based on data that never existed)

PostgreSQL không có dirty reads ở bất kỳ isolation level nào — minimum là READ COMMITTED. MySQL có Read Uncommitted nhưng gần như không ai dùng.

Non-Repeatable Read

Cùng một query trong cùng một transaction, chạy hai lần, trả về kết quả khác nhau.

Time    T1 (Report generation)     T2 (Order update)         DB
 1      BEGIN
 2      SELECT total FROM orders WHERE id=1  → 500k
 3                                 BEGIN
 4                                 UPDATE orders SET total=600k WHERE id=1
 5                                 COMMIT
 6      SELECT total FROM orders WHERE id=1  → 600k  ← Khác lần 1!
 7      COMMIT

T1 thấy hai giá trị khác nhau cho cùng row trong cùng transaction.

Production impact: Report chạy trong transaction dài, tổng cuối không bằng tổng của từng phần vì data thay đổi trong lúc report đang chạy.

Phantom Read

Query trả về tập rows khác nhau khi chạy lần hai, do transaction khác insert/delete rows.

Time    T1 (Inventory check)       T2 (New reservation)      DB
 1      BEGIN
 2      SELECT COUNT(*) FROM reservations
         WHERE product_id=5 AND date='2026-06-07'  → 9 (max là 10)
 3                                 BEGIN
 4                                 INSERT INTO reservations (product_id, date)
                                    VALUES (5, '2026-06-07')
 5                                 COMMIT
 6      SELECT COUNT(*) FROM reservations
         WHERE product_id=5 AND date='2026-06-07'  → 10  ← Phantom row!
 7      (T1 thấy có chỗ → book → thực ra đã đầy)
 8      INSERT INTO reservations... ← Overbooking!

Phantom read xảy ra với rows mới, non-repeatable read xảy ra với rows đã tồn tại bị modify.


Phần 4: Isolation Levels Deep Dive

Bốn mức độ isolation

LevelDirty ReadNon-Repeatable ReadPhantom Read
READ UNCOMMITTEDCó thểCó thểCó thể
READ COMMITTEDKhôngCó thểCó thể
REPEATABLE READKhôngKhôngCó thể*
SERIALIZABLEKhôngKhôngKhông

*PostgreSQL Repeatable Read thực tế ngăn phantom read do MVCC snapshot-based isolation.

READ COMMITTED — Default của PostgreSQL

Mỗi statement lấy snapshot mới. Transaction thấy tất cả commits xảy ra trước statement đó bắt đầu.

// Spring Boot — default isolation (READ COMMITTED)
@Transactional
public ReportResult generateReport(Long merchantId) {
    // Statement 1 lấy snapshot tại t=100
    long orderCount = orderRepo.countByMerchant(merchantId);
    
    // Giữa hai queries, transaction khác commit thêm orders
    
    // Statement 2 lấy snapshot mới tại t=105
    BigDecimal totalRevenue = orderRepo.sumRevenueByMerchant(merchantId);
    
    // orderCount và totalRevenue có thể inconsistent với nhau!
    return new ReportResult(orderCount, totalRevenue);
}

Khi nào chấp nhận được: Phần lớn OLTP operations — reads ngắn, single-row updates. Khi transaction ngắn, window cho inconsistency nhỏ.

Khi nào KHÔNG chấp nhận được: Report, audit, bất kỳ operation đọc nhiều rows và cần consistent view của toàn bộ dataset.

REPEATABLE READ

Toàn bộ transaction dùng cùng một snapshot — lấy tại thời điểm transaction bắt đầu.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public ReportResult generateReport(Long merchantId) {
    // Cả hai queries dùng snapshot tại thời điểm BEGIN
    long orderCount = orderRepo.countByMerchant(merchantId);   // consistent
    BigDecimal totalRevenue = orderRepo.sumRevenueByMerchant(merchantId); // consistent
    // Guaranteed: orderCount và totalRevenue nhất quán với nhau
    return new ReportResult(orderCount, totalRevenue);
}

Trade-off: Transaction dài hơn → old row versions phải giữ lâu hơn → VACUUM không thể cleanup → table bloat. Trong PostgreSQL, long REPEATABLE READ transactions làm autovacuum không effective, dẫn đến table phình to theo thời gian.

PostgreSQL vs MySQL:

  • PostgreSQL REPEATABLE READ: Snapshot-based, không dùng range locks. Không block concurrent writes.
  • MySQL InnoDB REPEATABLE READ: Dùng gap locks cho range queries. Có thể block inserts từ concurrent transactions.

SERIALIZABLE — Strong nhất, đắt nhất

Transactions hành xử như thể chạy hoàn toàn tuần tự, dù thực tế có thể concurrent. PostgreSQL implement bằng SSI (Serializable Snapshot Isolation) — phát hiện serialization conflicts và abort một transaction nếu cần.

@Transactional(isolation = Isolation.SERIALIZABLE)
public void bookLastTicket(Long eventId, Long userId) {
    int available = ticketRepo.countAvailable(eventId);
    if (available > 0) {
        ticketRepo.createBooking(eventId, userId);
        ticketRepo.decrementAvailable(eventId);
    }
}
// Nếu hai users gọi đồng thời:
// PostgreSQL phát hiện serialization conflict → abort một transaction
// Application nhận SerializationFailureException → phải retry

Khi nào cần SERIALIZABLE:

  • Booking/reservation systems (tránh overbooking)
  • Financial operations phức tạp
  • Bất kỳ operation có read-then-write dependency phức tạp

Trade-off:

  • Throughput giảm do conflict detection overhead
  • Application phải implement retry logic
  • Tỷ lệ abort tăng khi contention cao

Chọn isolation level đúng trong Spring

// Không set → dùng database default (PostgreSQL: READ COMMITTED)
@Transactional
public void defaultMethod() { ... }

// Explicit
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void consistentReport() { ... }

// Quarkus — tương tự
@Transactional
@io.quarkus.narayana.jta.QuarkusTransaction
public void quarkusMethod() { ... }
// Isolation level qua JPA properties hoặc raw JDBC

Phần 5: Spring Transaction Management

Tại sao Spring tạo @Transactional

Trước Spring, code transaction management bằng tay:

// Trước Spring — JDBC boilerplate:
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
try {
    // business logic
    conn.commit();
} catch (Exception e) {
    conn.rollback();
    throw e;
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

Vấn đề: boilerplate này phải lặp lại ở mọi service method. Nếu method A gọi method B, cả hai cần transaction → phải truyền Connection object qua tất cả call chain. Không maintainable.

Spring giải quyết với AOP: annotation @Transactional → Spring interceptor wraps method trong transaction management code tự động.

Proxy Mechanism — Điều thực sự xảy ra

@Service
public class OrderService {
    @Transactional
    public void placeOrder(OrderRequest request) {
        // Business logic
    }
}

// Khi Spring khởi động, nó tạo:
class OrderService$$SpringProxy extends OrderService {
    @Override
    public void placeOrder(OrderRequest request) {
        TransactionStatus status = transactionManager.getTransaction(txDef);
        try {
            super.placeOrder(request); // gọi method thực
            transactionManager.commit(status);
        } catch (RuntimeException e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
}

// Bean được inject là proxy, không phải OrderService trực tiếp:
@Autowired
private OrderService orderService; // Thực ra là OrderService$$SpringProxy
Component A        Spring Proxy                OrderService
    │                   │                           │
    │── placeOrder() ──>│                           │
    │                   │── getTransaction() ──────>│ (TransactionManager)
    │                   │<── TransactionStatus ─────│
    │                   │                           │
    │                   │── super.placeOrder() ────>│
    │                   │                           │── (SQL operations)
    │                   │<── return ────────────────│
    │                   │                           │
    │                   │── commit() ──────────────>│
    │<── return ────────│                           │

JpaTransactionManager vs DataSourceTransactionManager

// DataSourceTransactionManager — dùng với JDBC thuần
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

// JpaTransactionManager — dùng với Hibernate/JPA
// Spring Boot tự cấu hình nếu có JPA dependency
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
    JpaTransactionManager tm = new JpaTransactionManager(emf);
    tm.setEntityManagerFactory(emf);
    return tm;
}
// JpaTransactionManager đồng bộ hóa JPA EntityManager với JDBC transaction
// → Cả JPA và JDBC operations trong cùng transaction sẽ dùng cùng Connection

Hidden cost quan trọng: JpaTransactionManager phải tạo EntityManager cho mỗi transaction. EntityManager creation không free — nó allocate persistence context, flush queue, v.v. Với hàng nghìn transactions/giây, overhead này đáng kể.


Phần 6: @Transactional Pitfalls Phổ Biến

Pitfall 1: Self-Invocation Problem

@Service
public class OrderService {
    @Transactional
    public void placeOrder(OrderRequest request) {
        createOrder(request);
        reserveInventory(request);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void reserveInventory(OrderRequest request) {
        // Muốn chạy trong transaction riêng để log riêng
        // NHƯNG: gọi từ cùng class → bypass proxy → KHÔNG có transaction mới!
        inventoryRepo.reserve(request);
        auditLog.record("RESERVED", request);
    }
}

// Tại sao? Vì this.reserveInventory() gọi method trực tiếp trên object,
// không qua Spring proxy. Proxy chỉ intercept calls từ bên ngoài class.

Fix 1: Inject self

@Service
public class OrderService {
    @Autowired
    private OrderService self; // inject proxy của chính mình

    @Transactional
    public void placeOrder(OrderRequest request) {
        createOrder(request);
        self.reserveInventory(request); // gọi qua proxy → REQUIRES_NEW có hiệu lực
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void reserveInventory(OrderRequest request) {
        inventoryRepo.reserve(request);
    }
}

Fix 2: Tách class (preferred)

@Service
public class OrderService {
    @Autowired
    private InventoryService inventoryService;

    @Transactional
    public void placeOrder(OrderRequest request) {
        createOrder(request);
        inventoryService.reserveInventory(request); // different bean → qua proxy
    }
}

@Service
public class InventoryService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void reserveInventory(OrderRequest request) {
        inventoryRepo.reserve(request);
    }
}

Pitfall 2: Private Method không có transaction

@Service
public class OrderService {
    @Transactional // ← KHÔNG CÓ HIỆU LỰC trên private method
    private void createOrderInternal(OrderRequest request) {
        // Spring proxy không thể override private method
        // → không có transaction
    }

    public void placeOrder(OrderRequest request) {
        createOrderInternal(request); // Gọi trực tiếp, không qua proxy
    }
}

Spring proxy dùng subclassing (CGLIB) hoặc interface proxy (JDK dynamic proxy). Cả hai đều không thể override private methods. Compiler không warn, runtime không throw — transaction annotation bị silently ignored.

Fix: Method cần transaction phải là public (hoặc ít nhất protected với CGLIB).

Pitfall 3: Checked Exception không trigger rollback

@Service
public class PaymentService {
    @Transactional
    public void processPayment(PaymentRequest request) throws PaymentException {
        chargeCard(request);
        updateBalance(request);
        // Nếu chargeCard throw PaymentException (checked) → KHÔNG rollback!
        // Spring mặc định chỉ rollback cho RuntimeException và Error
    }
}

// Fix:
@Transactional(rollbackFor = PaymentException.class)
public void processPayment(PaymentRequest request) throws PaymentException {
    chargeCard(request);
    updateBalance(request);
}

// Hoặc: rollback cho tất cả Exception
@Transactional(rollbackFor = Exception.class)
public void processPayment(PaymentRequest request) throws Exception { ... }

Tại sao Spring mặc định không rollback cho checked exception? Design decision của Spring dựa trên convention Java: checked exception = expected, recoverable error; unchecked = unexpected, non-recoverable. Trong thực tế, convention này không nhất quán — nên luôn explicit về rollbackFor.

Pitfall 4: @Async và @Transactional không kết hợp được

@Service
public class NotificationService {
    @Async
    @Transactional
    public void sendConfirmationEmail(Long orderId) {
        Order order = orderRepo.findById(orderId).orElseThrow();
        // order.getItems() → có thể throw LazyInitializationException
        // vì method chạy trong thread khác, không có HTTP request context,
        // và nếu OSIV enabled nó sẽ không work đúng ở đây
        emailService.send(buildEmail(order));
    }
}

// @Async chạy trong thread pool riêng → transaction context không được propagate
// Thread mới không có TransactionSynchronizationManager binding
// → @Transactional bị ignore hoặc tạo transaction mới không liên quan

Fix: Không mix @Async với @Transactional. Fetch data trong transaction trước, pass DTO vào async method.

@Service
public class OrderService {
    @Transactional
    public void confirmOrder(Long orderId) {
        Order order = orderRepo.findById(orderId)
            .orElseThrow();
        order.confirm();
        OrderConfirmedDto dto = OrderConfirmedDto.from(order); // Fetch all needed data
        notificationService.sendConfirmationEmail(dto); // Pass DTO, not entity
    }
}

@Service
public class NotificationService {
    @Async // Không cần @Transactional vì không làm DB operations
    public void sendConfirmationEmail(OrderConfirmedDto dto) {
        emailService.send(buildEmail(dto));
    }
}

Pitfall 5: Long-Running Transaction giữ Lock

@Transactional // transaction dài = connection và lock held lâu
public OrderResult processLargeOrder(LargeOrderRequest request) {
    // Step 1: Validate (fast, ~5ms)
    validateRequest(request);

    // Step 2: Call external pricing API (slow, ~200ms)
    PricingResult pricing = externalPricingService.calculate(request); // External call!

    // Step 3: Call inventory API (slow, ~150ms)
    InventoryResult inventory = externalInventoryService.check(request); // External call!

    // Step 4: Database operations (fast, ~10ms)
    Order order = createOrder(request, pricing, inventory);

    // Total transaction time: ~365ms
    // Connection held: 365ms
    // Locks held: 365ms
    // HikariCP pool exhaustion khi nhiều concurrent requests!
}

Fix: Chỉ wrap database operations trong transaction, external calls ra ngoài.

public OrderResult processLargeOrder(LargeOrderRequest request) {
    validateRequest(request);

    // External calls NGOÀI transaction
    PricingResult pricing = externalPricingService.calculate(request); // 200ms, không hold lock
    InventoryResult inventory = externalInventoryService.check(request); // 150ms, không hold lock

    // Chỉ DB operations trong transaction
    return createOrderTransactionally(request, pricing, inventory); // ~10ms
}

@Transactional
private OrderResult createOrderTransactionally(LargeOrderRequest request,
                                                PricingResult pricing,
                                                InventoryResult inventory) {
    return createOrder(request, pricing, inventory);
}

Pitfall 6: Nested @Transactional Misunderstanding

@Service
public class OrderService {
    @Transactional
    public void placeOrder(OrderRequest request) {
        Order order = createOrder(request);
        try {
            auditService.logOrderCreated(order.getId()); // REQUIRES_NEW
        } catch (Exception e) {
            log.warn("Audit logging failed, continuing...");
            // Tưởng rằng audit failure không ảnh hưởng order
        }
        // Order vẫn commit vì audit chạy trong transaction riêng
        // Đây là ĐÚNG nếu dùng REQUIRES_NEW
    }
}

@Service
public class AuditService {
    @Transactional(propagation = Propagation.REQUIRED) // ← BUG: REQUIRED thay vì REQUIRES_NEW
    public void logOrderCreated(Long orderId) {
        auditRepo.save(new AuditLog(orderId, "ORDER_CREATED"));
        // Nếu rollback → rollback cả order transaction của caller!
    }
}
// Với REQUIRED: auditService chạy trong CÙNG transaction với orderService
// Nếu auditService throw exception và caller catch → vẫn rollback toàn bộ
// vì transaction đã được marked "rollback-only"
// Fix: REQUIRES_NEW cho audit, vì audit failure không nên rollback business operation
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderCreated(Long orderId) {
    auditRepo.save(new AuditLog(orderId, "ORDER_CREATED"));
}

Phần 7: Transaction Propagation

REQUIRED — Default, join nếu có, tạo mới nếu không

@Transactional(propagation = Propagation.REQUIRED) // default
public void methodA() {
    methodB(); // methodB join transaction của methodA
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
    // Nếu methodA có transaction → methodB dùng cùng transaction
    // Nếu không có transaction → methodB tạo transaction mới
}
Có transaction caller:    Không có transaction caller:
[---methodA txn---]       [---methodB txn (new)---]
  [methodB joins]           methodB

Khi nào dùng: Mặc định cho service methods. Business operations phải atomic với caller.

Vấn đề tinh tế: Nếu methodB rollback (ví dụ throw unchecked exception), nó rollback toàn bộ transaction của methodA dù methodA catch exception đó. Transaction đã được marked rollback-only, không thể commit sau đó.

@Transactional
public void methodA() {
    try {
        methodB(); // methodB throw và rollback
    } catch (Exception e) {
        // Tưởng rằng đã handle, methodA tiếp tục
    }
    orderRepo.save(order); // ← throw UnexpectedRollbackException!
    // Transaction đã bị mark rollback-only bởi methodB
}

REQUIRES_NEW — Luôn tạo transaction mới

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String action) {
    // Suspend transaction của caller nếu có
    // Tạo transaction mới, hoàn toàn độc lập
    // Commit/rollback không ảnh hưởng caller's transaction
    auditRepo.save(new AuditEntry(action));
}
[---caller txn (suspended)---]
    [---new txn---]
    [---commit or rollback---]
[---caller txn (resumed)------]

Khi nào dùng:

  • Audit logging (không muốn audit mất khi business tx rollback)
  • Sending notifications (chỉ send sau confirm)
  • Operations phải commit independently

Trade-off: Tốn thêm một connection từ pool cho transaction mới (vì transaction cũ vẫn đang hold connection). Với REQUIRES_NEW lồng nhau nhiều tầng, có thể exhaust connection pool.

NESTED — Savepoint trong transaction cha

@Transactional(propagation = Propagation.NESTED)
public void nestedOperation() {
    // Tạo SAVEPOINT trong transaction cha
    // Rollback chỉ về SAVEPOINT, không rollback toàn bộ
    // Commit chỉ xảy ra khi transaction cha commit
}
[---outer txn---]
    SAVEPOINT sp1
    [---nested operation---]
    RELEASE SAVEPOINT sp1 (nếu OK)
    hoặc ROLLBACK TO SAVEPOINT sp1 (nếu fail)
[---outer txn commit hoặc rollback toàn bộ---]

Khi nào dùng: Partial retry trong batch — nếu một item fail, rollback chỉ item đó, tiếp tục với items còn lại.

Không dùng: Khi cần true isolation (NESTED vẫn nằm trong outer transaction, không tạo transaction độc lập như REQUIRES_NEW).

SUPPORTS, NOT_SUPPORTED, MANDATORY, NEVER

// SUPPORTS: Dùng transaction nếu có, không tạo mới nếu không có
@Transactional(propagation = Propagation.SUPPORTS)
public List<Order> readOrders() {
    // Phù hợp cho read operations không cần transaction nhưng benefi từ nó nếu sẵn có
}

// NOT_SUPPORTED: Suspend transaction nếu có, chạy không có transaction
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void longRunningRead() {
    // Suspend caller transaction để không hold connection
    // Không cần transaction cho pure read
}

// MANDATORY: Phải có transaction từ caller, nếu không throw
@Transactional(propagation = Propagation.MANDATORY)
public void criticalDbWrite() {
    // Đảm bảo caller không gọi method này ngoài transaction
    // Fail fast thay vì data corruption
}

// NEVER: Không được có transaction, nếu có throw
@Transactional(propagation = Propagation.NEVER)
public void cacheLoad() {
    // Đảm bảo operation này không bị bao trong transaction
    // Ví dụ: cache warming không cần (và không muốn) transaction overhead
}

Phần 8: Hibernate và JPA Transaction Internals

Persistence Context — Trung tâm của mọi thứ

Persistence context là identity map + change tracker cho tất cả entities trong một unit of work. Mọi entity được load trong một session đều được track.

@Transactional
public void updateOrder(Long orderId) {
    // Load entity → Hibernate lưu reference trong persistence context
    Order order = orderRepo.findById(orderId).orElseThrow();
    // Hibernate cũng lưu snapshot của state ban đầu

    // Modify entity
    order.setStatus("PROCESSING");
    order.setProcessedAt(Instant.now());

    // KHÔNG cần save() — Hibernate sẽ detect changes tự động khi flush
    // (dirty checking tại thời điểm flush)
}
// Khi @Transactional method return → flush → commit
// Hibernate compare current state vs snapshot → tạo UPDATE SQL
// → UPDATE orders SET status='PROCESSING', processed_at=... WHERE id=?

Entity States và Transitions

[New / Transient]
     │ entityManager.persist(entity)
[Managed / Persistent]  ←─── entityManager.find() / JPQL query
     │                         (trong active session)
     │ entityManager.detach(entity)
     │ session closes
[Detached]
     │ entityManager.merge(detachedEntity)
[Managed / Persistent]
     │ entityManager.remove(entity)
[Removed]
     │ flush / commit
[Deleted from DB]
@Transactional
public void demonstrateStates(Long orderId) {
    // MANAGED state — Hibernate tracks mọi thay đổi
    Order managed = em.find(Order.class, orderId);
    managed.setStatus("UPDATED"); // Sẽ generate UPDATE khi flush

    // DETACHED — Hibernate không track
    em.detach(managed);
    managed.setStatus("DETACHED_CHANGE"); // Không có SQL nào được generate

    // MERGE — Hibernate load lại và apply changes
    Order reattached = em.merge(managed);
    // Hibernate execute: SELECT * FROM orders WHERE id=?
    // Rồi apply state từ managed object → tạo UPDATE
}

Flush — Khi nào SQL thực sự chạy?

Đây là nguồn gốc của nhiều bug khó hiểu: SQL không chạy ngay khi bạn gọi save() hay persist().

@Transactional
public void bugScenario() {
    Order order = new Order("PENDING");
    orderRepo.save(order); // SQL chưa chạy! Chỉ queue trong persistence context

    // Nếu bạn query ngay sau đó với native SQL:
    int count = jdbcTemplate.queryForObject(
        "SELECT COUNT(*) FROM orders WHERE status='PENDING'", Integer.class);
    // count có thể là 0 vì INSERT chưa được flush xuống DB!
}

FlushMode:

  • AUTO (default): Flush trước mỗi query (để query thấy changes) và trước commit
  • COMMIT: Chỉ flush trước commit — không đảm bảo query thấy changes mới nhất
  • MANUAL: Chỉ flush khi gọi entityManager.flush() explicit
// Manual flush khi cần control chính xác:
@Transactional
public void batchInsert(List<OrderData> data) {
    for (int i = 0; i < data.size(); i++) {
        Order order = new Order(data.get(i));
        em.persist(order);

        if (i % 50 == 0) {
            em.flush();  // Flush batch xuống DB
            em.clear();  // Clear persistence context để tránh memory bloat
        }
    }
}

Dirty Checking — Chi phí ẩn của convenience

Hibernate detect changes bằng cách so sánh current state với snapshot được lưu khi entity được load. Điều này xảy ra tại mỗi flush.

@Transactional
public void loadManyEntities() {
    // Load 10,000 orders vào persistence context
    List<Order> orders = orderRepo.findAll();
    // Hibernate lưu 10,000 snapshots

    // ... một số logic không thay đổi orders ...

    // Khi flush: Hibernate so sánh 10,000 current states vs 10,000 snapshots
    // Dù không có gì thay đổi, overhead vẫn xảy ra
}

// Fix: @Immutable hoặc StatelessSession cho read-only bulk operations
@Entity
@org.hibernate.annotations.Immutable
public class ProductCatalog {
    // Không bao giờ được modify → Hibernate skip dirty checking
}

Phần 9: Locking Strategies

Optimistic Locking — Assume no conflict, detect khi commit

Optimistic locking không lock row khi đọc. Thay vào đó, nó track version của data và fail commit nếu version đã thay đổi.

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

    private BigDecimal balance;

    @Version // Hibernate tự động increment khi update
    private Long version;
}

// Thread 1 và Thread 2 đọc cùng account (version=5)
// Thread 1 update trước → version trở thành 6
// Thread 2 cố update với WHERE id=? AND version=5 → 0 rows affected
// → OptimisticLockException
-- SQL được Hibernate generate:
UPDATE accounts
SET balance = 900000, version = 6
WHERE id = 123 AND version = 5; -- ← Optimistic check
-- Nếu 0 rows affected → OptimisticLockException

Retry strategy:

@Service
public class AccountService {
    @Transactional
    @Retryable(
        retryFor = OptimisticLockingFailureException.class,
        maxAttempts = 3,
        backoff = @Backoff(delay = 100, multiplier = 2)
    )
    public void transferFunds(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findById(fromId).orElseThrow();
        Account to = accountRepo.findById(toId).orElseThrow();
        from.deduct(amount);
        to.add(amount);
    }
    // Nếu conflict → retry tối đa 3 lần với exponential backoff
}

Khi nào dùng Optimistic Locking:

  • Conflict hiếm (low contention)
  • Read » Write (ví dụ: product catalog updates)
  • User-facing forms (edit và save)

Khi nào KHÔNG dùng:

  • High contention (many concurrent updates to same rows)
  • Retry không phù hợp với use case
  • Khi cần guarantee không bao giờ conflict (tài khoản ngân hàng với nhiều concurrent transfers)

Pessimistic Locking — Lock ngay khi đọc

// PESSIMISTIC_WRITE: SELECT ... FOR UPDATE
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findByIdForUpdate(@Param("id") Long id);

@Transactional
public void transferFunds(Long fromId, Long toId, BigDecimal amount) {
    // Lock cả hai accounts ngay khi đọc
    // Đảm bảo không ai khác có thể modify trong lúc ta đang xử lý
    Long firstId = Math.min(fromId, toId);   // Consistent lock ordering!
    Long secondId = Math.max(fromId, toId);  // Tránh deadlock

    Account first = accountRepo.findByIdForUpdate(firstId);
    Account second = accountRepo.findByIdForUpdate(secondId);

    if (firstId.equals(fromId)) {
        first.deduct(amount);
        second.add(amount);
    } else {
        second.deduct(amount);
        first.add(amount);
    }
}
-- SQL generated:
SELECT * FROM accounts WHERE id = ? FOR UPDATE;
-- Row bị lock cho đến khi transaction commit/rollback
-- Concurrent transactions sẽ block tại câu lệnh này

PESSIMISTIC_READ vs PESSIMISTIC_WRITE:

// PESSIMISTIC_READ: SELECT ... FOR SHARE
// Cho phép concurrent reads, block writes
// Dùng khi cần đảm bảo data không thay đổi trong khi bạn đọc để tính toán

@Lock(LockModeType.PESSIMISTIC_READ)
Account findByIdForShare(@Param("id") Long id);
// → SELECT ... FOR SHARE (PostgreSQL)
// Nhiều transactions có thể hold SHARE lock đồng thời
// Chỉ WRITE lock (FOR UPDATE) phải đợi

Deadlock — Khi hai transactions chờ nhau

T1 lock Account A → chờ lock Account B
T2 lock Account B → chờ lock Account A
→ Deadlock! Cả hai chờ nhau mãi mãi

PostgreSQL phát hiện deadlock tự động (timeout-based hoặc dependency graph) và abort một transaction:

ERROR: deadlock detected
DETAIL: Process 1234 waits for ShareLock on transaction 5678; blocked by process 5678.
        Process 5678 waits for ShareLock on transaction 1234; blocked by process 1234.
HINT: See server log for query details.

Prevention:

// 1. Consistent lock ordering (đã thấy ở trên)
// 2. Giảm lock scope và thời gian

// 3. Timeout với lock acquisition
@Transactional
public void processWithTimeout(Long orderId) {
    // PostgreSQL: SET lock_timeout = '5s' trước query
    // Nếu không acquire lock trong 5s → throw LockTimeoutException thay vì deadlock
}

// application.properties
spring.jpa.properties.jakarta.persistence.lock.timeout=5000 // 5 seconds

Phần 10: Transaction Trong Microservices

Tại sao ACID không work across services

Trong monolith, tất cả operations dùng cùng database → một transaction bao phủ tất cả. Trong microservices:

Order Service ───→ Order DB (PostgreSQL)
Payment Service ──→ Payment DB (PostgreSQL)
Inventory Service → Inventory DB (PostgreSQL)

// Không có cơ chế để có một transaction bao phủ cả ba DB
// Mỗi service có transaction riêng, commit riêng

Failure scenario không có distributed transaction:

1. Order Service: CREATE order → commit ✓
2. Payment Service: CHARGE payment → commit ✓
3. Inventory Service: DEDUCT inventory → FAIL ✗

Trạng thái: Order tồn tại, Tiền đã thu, Inventory chưa deduct
→ Inconsistent state không thể tự recovery

Two-Phase Commit (2PC) — Tại sao không phổ biến

2PC là giao thức để coordinate distributed transaction:

Phase 1 (Prepare):
Coordinator → Order Service: "Prepare to commit"
Coordinator → Payment Service: "Prepare to commit"
Coordinator → Inventory Service: "Prepare to commit"

Nếu tất cả trả về "Ready":

Phase 2 (Commit):
Coordinator → tất cả: "Commit now"

Vấn đề:

  1. Blocking: Trong Phase 1, tất cả services lock resources. Nếu coordinator crash giữa Phase 1 và 2 → resources bị lock vô thời hạn.

  2. Coordinator single point of failure: Nếu coordinator crash sau Phase 1 nhưng trước Phase 2 → participants không biết commit hay rollback.

  3. Network partition: Message commit không đến được một service → service không biết phải làm gì.

  4. Performance: Minimum 4 round-trips network cho mỗi transaction. Mỗi round-trip thêm latency, hold locks lâu hơn.

2PC tồn tại (JTA/XA trong Java) nhưng gần như không dùng trong microservices vì những trade-offs này.

CAP Theorem — Tại sao phải chọn

CAP theorem: trong distributed system, chỉ có thể đảm bảo tối đa 2 trong 3:

  • Consistency: Mọi read thấy write gần nhất
  • Availability: Mọi request đều nhận response (không phải error)
  • Partition Tolerance: Hệ thống tiếp tục hoạt động dù network partition

Network partition không thể tránh trong production → phải chọn giữa C và A. Microservices thường chọn AP (availability và partition tolerance) và accept eventual consistency.


Phần 11: Saga Pattern

Tại sao Saga tồn tại

Saga là chuỗi local transactions, mỗi transaction trong một service, với compensation transaction để undo nếu cần.

Saga: Place Order

Step 1: Order Service     → CREATE order (local tx)
Step 2: Inventory Service → RESERVE inventory (local tx)
Step 3: Payment Service   → CHARGE payment (local tx)
Step 4: Shipping Service  → SCHEDULE shipment (local tx)

Nếu Payment FAIL:
Compensation Step 3: Payment Service → VOID charge
Compensation Step 2: Inventory Service → RELEASE reservation
Compensation Step 1: Order Service → CANCEL order

Choreography vs Orchestration

Choreography — Services tự phối hợp qua events:

Order Service: CREATE order → publish OrderCreated event
Inventory Service: consume OrderCreated → RESERVE → publish InventoryReserved
Payment Service: consume InventoryReserved → CHARGE → publish PaymentCharged
Shipping Service: consume PaymentCharged → SCHEDULE shipment

Failure:
Payment Service: CHARGE fail → publish PaymentFailed
Inventory Service: consume PaymentFailed → RELEASE reservation → publish InventoryReleased
Order Service: consume InventoryReleased → CANCEL order
// Choreography với Spring Events (trong-process) hoặc Kafka (cross-service)
@Service
public class PaymentService {
    @Transactional
    @KafkaListener(topics = "inventory.reserved")
    public void handleInventoryReserved(InventoryReservedEvent event) {
        try {
            PaymentResult result = chargePayment(event.getOrderId(), event.getAmount());
            kafkaTemplate.send("payment.charged",
                new PaymentChargedEvent(event.getOrderId(), result.getTransactionId()));
        } catch (PaymentException e) {
            kafkaTemplate.send("payment.failed",
                new PaymentFailedEvent(event.getOrderId(), e.getReason()));
        }
    }
}

Orchestration — Một Orchestrator điều phối toàn bộ flow:

Order Orchestrator:
  Step 1: gọi Order Service → CREATE
  Step 2: gọi Inventory Service → RESERVE
  Step 3: gọi Payment Service → CHARGE
  Step 4: gọi Shipping Service → SCHEDULE

  Nếu Step 3 fail:
    gọi Inventory Service → RELEASE (compensation)
    gọi Order Service → CANCEL (compensation)
// Orchestration với Spring State Machine hoặc Temporal
@Component
public class PlaceOrderSaga {
    @Autowired private OrderClient orderClient;
    @Autowired private InventoryClient inventoryClient;
    @Autowired private PaymentClient paymentClient;

    @Transactional
    public void execute(PlaceOrderCommand command) {
        SagaState state = sagaStateRepo.create(command);

        try {
            // Step 1
            state.setOrderId(orderClient.createOrder(command));
            sagaStateRepo.save(state);

            // Step 2
            state.setReservationId(inventoryClient.reserve(state.getOrderId(), command.getItems()));
            sagaStateRepo.save(state);

            // Step 3
            state.setPaymentId(paymentClient.charge(state.getOrderId(), command.getAmount()));
            sagaStateRepo.save(state);

            state.markCompleted();
            sagaStateRepo.save(state);

        } catch (Exception e) {
            compensate(state);
            throw e;
        }
    }

    private void compensate(SagaState state) {
        if (state.getPaymentId() != null) {
            paymentClient.void_(state.getPaymentId());
        }
        if (state.getReservationId() != null) {
            inventoryClient.release(state.getReservationId());
        }
        if (state.getOrderId() != null) {
            orderClient.cancel(state.getOrderId());
        }
    }
}

Choreography vs Orchestration — Trade-offs:

ChoreographyOrchestration
CouplingLoose (events)Tighter (direct calls)
VisibilityKhó trace flowDễ trace (single place)
DebuggingKhóDễ
Failure handlingPhân tán, khó đảm bảoTập trung, dễ đảm bảo
ScalabilityTốtOrchestrator là bottleneck tiềm tàng

Idempotency là bắt buộc trong Saga:

// Mỗi step phải idempotent — safe to retry
@Transactional
@KafkaListener(topics = "payment.charge.requested")
public void chargePayment(ChargePaymentCommand command) {
    // Check idempotency key trước khi process
    if (paymentRepo.existsByIdempotencyKey(command.getIdempotencyKey())) {
        // Already processed, return existing result
        return;
    }

    Payment payment = new Payment(command);
    payment.setIdempotencyKey(command.getIdempotencyKey());
    paymentRepo.save(payment);
    // Process payment...
}

Phần 12: Transactional Outbox Pattern

Vấn đề kinh điển: Dual Write

@Transactional
public void placeOrder(OrderRequest request) {
    Order order = createOrder(request); // DB write
    orderRepo.save(order);
    // COMMIT (DB OK)

    // Publish message NGOÀI transaction
    kafkaTemplate.send("order.placed", new OrderPlacedEvent(order.getId()));
    // Nếu Kafka publish fail → DB committed, message không có
    // Order tồn tại nhưng downstream services không biết
}

Ngược lại:

// Publish trước, commit sau:
kafkaTemplate.send("order.placed", event);
// Kafka OK
// DB commit fail → Order không tồn tại, nhưng message đã published
// Downstream services xử lý order không tồn tại

Không có cách nào đảm bảo atomicity giữa DB write và message publish với Kafka thông thường.

Transactional Outbox — Giải pháp

Thay vì publish trực tiếp, ghi message vào outbox table trong cùng transaction với business data. Một process riêng đọc outbox và publish.

-- Schema
CREATE TABLE outbox_messages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    aggregate_type VARCHAR(100) NOT NULL,  -- 'ORDER'
    aggregate_id VARCHAR(100) NOT NULL,    -- order ID
    event_type VARCHAR(100) NOT NULL,      -- 'ORDER_PLACED'
    payload JSONB NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    published_at TIMESTAMP WITH TIME ZONE, -- NULL = not yet published
    retry_count INT DEFAULT 0
);

CREATE INDEX idx_outbox_unpublished ON outbox_messages (created_at)
WHERE published_at IS NULL;
@Transactional
public void placeOrder(OrderRequest request) {
    Order order = createOrder(request);
    orderRepo.save(order);

    // Ghi outbox message trong CÙNG transaction
    OutboxMessage message = OutboxMessage.builder()
        .aggregateType("ORDER")
        .aggregateId(order.getId().toString())
        .eventType("ORDER_PLACED")
        .payload(objectMapper.writeValueAsString(new OrderPlacedEvent(order)))
        .build();
    outboxRepo.save(message);
    // Cả order và outbox message được commit atomically
    // Hoặc cả hai rollback — không có dual write problem
}
// Outbox poller — chạy riêng biệt
@Component
public class OutboxPoller {
    @Scheduled(fixedDelay = 1000) // Mỗi giây
    @Transactional
    public void pollAndPublish() {
        List<OutboxMessage> pending = outboxRepo.findUnpublished(50);

        for (OutboxMessage msg : pending) {
            try {
                kafkaTemplate.send(
                    topicFor(msg.getEventType()),
                    msg.getAggregateId(),
                    msg.getPayload()
                ).get(5, TimeUnit.SECONDS); // Synchronous send với timeout

                msg.markPublished();
                outboxRepo.save(msg);
            } catch (Exception e) {
                msg.incrementRetry();
                outboxRepo.save(msg);
                log.error("Failed to publish outbox message {}", msg.getId(), e);
            }
        }
    }
}

Debezium — Change Data Capture thay vì polling:

Debezium đọc PostgreSQL WAL (write-ahead log) và publish changes vào Kafka. Không cần polling, latency thấp hơn (~ms thay vì ~1s), không tạo thêm load cho DB.

# Debezium connector configuration
{
  "name": "outbox-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres",
    "database.dbname": "orders",
    "table.include.list": "public.outbox_messages",
    "transforms": "outbox",
    "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter"
  }
}

Tại sao nhiều công ty dùng Outbox Pattern:

  • Amazon, Uber, Netflix — đều document việc dùng pattern này
  • Guarantee at-least-once delivery (kết hợp idempotency ở consumer → exactly-once semantics)
  • Không cần distributed transaction hay 2PC
  • Auditable: outbox table là history của mọi event được gửi

Phần 13: Production Debugging và Monitoring

Điều tra Lock Contention và Deadlock

-- Xem tất cả active locks và ai đang block ai
SELECT
    blocked.pid AS blocked_pid,
    blocked.usename AS blocked_user,
    blocked.query AS blocked_query,
    now() - blocked.query_start AS blocked_duration,
    blocking.pid AS blocking_pid,
    blocking.query AS blocking_query,
    blocking.state AS blocking_state
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
    ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
ORDER BY blocked_duration DESC;
-- Xem long-running transactions (> 1 phút)
SELECT
    pid,
    now() - xact_start AS txn_duration,
    now() - query_start AS query_duration,
    state,
    wait_event_type,
    wait_event,
    left(query, 100) AS query_snippet
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
  AND now() - xact_start > INTERVAL '1 minute'
ORDER BY txn_duration DESC;
-- Kill long-running transaction (emergency)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE now() - xact_start > INTERVAL '10 minutes'
  AND state != 'idle';

Connection Exhaustion Investigation

// HikariCP metrics qua Micrometer
@Bean
public MeterRegistryCustomizer<MeterRegistry> hikariMetrics(DataSource dataSource) {
    return registry -> {
        if (dataSource instanceof HikariDataSource ds) {
            ds.setMetricRegistry(registry);
        }
    };
}

// Metrics:
// hikaricp.connections.active   → connections đang được dùng
// hikaricp.connections.pending  → requests chờ connection (CRITICAL nếu > 0)
// hikaricp.connections.timeout  → số lần timeout acquiring connection
// hikaricp.connections.acquire  → histogram, thời gian acquire connection

// Alert khi pending > 5 trong 30 giây → pool đang exhausted
// Enable leak detection
# application.yml
spring:
  datasource:
    hikari:
      leak-detection-threshold: 30000  # 30 giây không release → log warning

Hibernate Statistics trong Production

// Cẩn thận: generate_statistics có overhead trong production
// Chỉ enable khi cần debug, disable sau

@Scheduled(fixedRate = 60000) // Mỗi phút
public void logHibernateStats() {
    Statistics stats = sessionFactory.getStatistics();
    long queryCount = stats.getQueryExecutionCount();
    long slowQueries = stats.getQueryExecutionMaxTime();

    if (slowQueries > 1000) { // > 1 giây
        log.warn("Slow query detected: {}ms, SQL: {}",
            slowQueries, stats.getQueryExecutionMaxTimeQueryString());
    }

    // Reset mỗi interval để tính rate
    stats.clear();
}

Slow Transaction Detection với Micrometer

@Aspect
@Component
public class TransactionMonitoringAspect {
    private final MeterRegistry meterRegistry;

    @Around("@annotation(transactional)")
    public Object monitorTransaction(ProceedingJoinPoint pjp,
                                      Transactional transactional) throws Throwable {
        Timer.Sample sample = Timer.start(meterRegistry);
        String methodName = pjp.getSignature().toShortString();
        try {
            Object result = pjp.proceed();
            sample.stop(Timer.builder("transaction.duration")
                .tag("method", methodName)
                .tag("outcome", "success")
                .register(meterRegistry));
            return result;
        } catch (Exception e) {
            sample.stop(Timer.builder("transaction.duration")
                .tag("method", methodName)
                .tag("outcome", "failure")
                .register(meterRegistry));
            throw e;
        }
    }
}

Grafana Alert Rules:

# Transaction duration P99 > 5 giây
- alert: SlowTransactions
  expr: histogram_quantile(0.99, rate(transaction_duration_seconds_bucket[5m])) > 5
  for: 2m
  labels:
    severity: warning

# Connection pool pending > 0
- alert: ConnectionPoolPressure
  expr: hikaricp_connections_pending > 0
  for: 30s
  labels:
    severity: critical

Phần 14: Production Incidents Thực Tế

Incident 1: Tiền được transfer hai lần

Triệu chứng: Customer báo cáo bị deduct tiền hai lần cho một transaction. Số tiền chính xác như nhau. Timestamp cách nhau 200ms.

Investigation:

-- Tìm duplicate payments trong cùng khoảng thời gian ngắn
SELECT
    account_id,
    amount,
    COUNT(*) as count,
    MIN(created_at) as first,
    MAX(created_at) as last
FROM payment_transactions
WHERE created_at > NOW() - INTERVAL '1 hour'
GROUP BY account_id, amount
HAVING COUNT(*) > 1;

Root cause: Frontend retry logic + không idempotent backend.

User click "Pay" button → Request 1 gửi
Network timeout (30s) → User thấy error → Click lại
Request 1 vẫn đang xử lý (chậm vì external payment gateway)
Request 2 cũng được xử lý
Cả hai succeed → Hai payment

Fix:

@Transactional
public PaymentResult processPayment(PaymentRequest request) {
    // Check idempotency key TRƯỚC khi process
    Optional<Payment> existing = paymentRepo.findByIdempotencyKey(
        request.getIdempotencyKey()
    );
    if (existing.isPresent()) {
        return PaymentResult.from(existing.get()); // Return previous result
    }

    Payment payment = createPayment(request);
    payment.setIdempotencyKey(request.getIdempotencyKey());
    paymentRepo.save(payment);
    return PaymentResult.from(payment);
}

// Database constraint đảm bảo idempotency key unique dù race condition:
// CREATE UNIQUE INDEX idx_payments_idempotency ON payments (idempotency_key);

Incident 2: Inventory trở thành số âm

Triệu chứng: inventory_stock của product X = -47 sau flash sale. 147 orders thành công cho product chỉ có 100 stock.

Root cause: Lost update do thiếu locking.

// Code lỗi:
@Transactional
public void reserveInventory(Long productId, int quantity) {
    Product product = productRepo.findById(productId).orElseThrow();
    // 100 concurrent requests đọc cùng stock = 100
    if (product.getStock() < quantity) {
        throw new InsufficientStockException();
    }
    product.setStock(product.getStock() - quantity); // Lost update!
    // Mỗi request set stock = 100 - 1 = 99, 99 - 1 = 98, ...
    // Nhưng concurrent reads không thấy nhau's writes
    productRepo.save(product);
}

Fix:

@Transactional
public void reserveInventory(Long productId, int quantity) {
    // Atomic UPDATE với check — không đọc trước
    int updated = productRepo.decrementStockIfAvailable(productId, quantity);
    if (updated == 0) {
        throw new InsufficientStockException();
    }
}

// Repository:
@Modifying
@Query("""
    UPDATE Product p
    SET p.stock = p.stock - :quantity
    WHERE p.id = :productId AND p.stock >= :quantity
    """)
int decrementStockIfAvailable(@Param("productId") Long productId,
                               @Param("quantity") int quantity);
// Atomic: check và update trong một SQL statement
// Database-level atomicity đảm bảo không có race condition

Incident 3: Order tạo nhưng không có payment

Triệu chứng: Mỗi ngày có ~50 orders ở trạng thái “CREATED” nhưng không có payment record tương ứng.

Root cause: Checked exception không trigger rollback.

@Transactional
public void placeOrder(OrderRequest request) throws PaymentException {
    Order order = createAndSaveOrder(request);
    // ORDER đã được lưu trong transaction

    try {
        paymentService.charge(order.getId(), request.getAmount());
        // PaymentException (checked) được catch ở đây
    } catch (PaymentException e) {
        log.error("Payment failed for order {}", order.getId(), e);
        // Developer tưởng transaction sẽ rollback
        // NHƯNG: checked exception KHÔNG trigger rollback mặc định!
        throw e; // Throw lại, nhưng transaction đã không rollback
    }
}
// Result: ORDER commit thành công, payment fail → inconsistent state

Fix:

@Transactional(rollbackFor = PaymentException.class) // Explicit rollback
public void placeOrder(OrderRequest request) throws PaymentException {
    Order order = createAndSaveOrder(request);
    paymentService.charge(order.getId(), request.getAmount());
    // Nếu PaymentException → rollback cả order
}

Incident 4: Connection pool exhausted trong peak hours

Triệu chứng: 9:00 AM mỗi ngày, tất cả requests bắt đầu timeout. Metrics cho thấy hikaricp_connections_pending tăng đột biến. Database CPU thấp — database không bận.

Root cause: Transaction scope quá rộng bao gồm email sending.

@Transactional
public RegistrationResult registerUser(RegistrationRequest request) {
    User user = createUser(request);
    userRepo.save(user);
    sendWelcomeEmail(user); // SMTP call: ~2-5 giây, đôi khi timeout 30 giây!
    // Connection bị hold trong suốt SMTP call
    return RegistrationResult.success(user.getId());
}

9 AM là giờ nhiều người đăng ký. Mỗi registration hold connection 2-5 giây. Pool size 10 → chỉ cần 10 concurrent registrations là pool exhausted. Mọi request sau đó chờ → timeout cascade.

Fix:

@Transactional
public RegistrationResult registerUser(RegistrationRequest request) {
    User user = createUser(request);
    userRepo.save(user);
    return RegistrationResult.success(user.getId());
    // Transaction commit ở đây, connection released
}

// Gọi email NGOÀI transaction
public RegistrationResult register(RegistrationRequest request) {
    RegistrationResult result = registerUser(request); // transaction
    sendWelcomeEmail(result.getUserId()); // sau khi transaction commit
    return result;
}

Phần 15: Senior Engineer Transaction Checklist

Trước khi viết transaction

□ Operation này có thực sự cần transaction không?
  - Nếu chỉ là single SQL statement → DB đã atomic
  - Nếu là pure read → @Transactional(readOnly=true) hoặc không cần

□ Isolation level nào phù hợp?
  - READ COMMITTED: Default, phần lớn OLTP operations
  - REPEATABLE READ: Consistent snapshot (report, audit)
  - SERIALIZABLE: Critical correctness (booking, financial)

□ Scope của transaction — chỉ bao gồm:
  - Database operations
  - Không bao gồm: external API calls, file I/O, email/SMS, heavy computation

□ Có cần idempotency không?
  - Nếu operation có thể được retry → cần idempotency key

Trong quá trình implement

□ @Transactional(rollbackFor = Exception.class) nếu dùng checked exceptions
□ Propagation được chọn có chủ ý (không rely vào REQUIRED blindly)
□ Không có self-invocation (gọi @Transactional method trong cùng class)
□ Method là public (Spring proxy không intercept private/package-private)
□ Không mix @Async với @Transactional
□ Locking strategy được chọn đúng:
  - Optimistic: low contention, retry acceptable
  - Pessimistic: high contention, correctness critical
□ Lock ordering consistent nếu lock nhiều entities (tránh deadlock)
□ readOnly=true cho tất cả read-only service methods

Code Review Red Flags

□ @Transactional ở tất cả methods mà không xem xét scope
□ External API calls bên trong @Transactional method
□ try-catch nuốt exception mà không rethrow → có thể che giấu rollback
□ @Transactional trên private method (silently no-op)
□ Checked exception không được khai báo trong rollbackFor
□ EAGER fetching ở OneToMany/ManyToMany
□ N+1 queries không được detect (cần Hibernate statistics)
□ Outbox pattern thiếu cho critical events
□ Thiếu idempotency key cho payment/financial operations

Production Deployment Checklist

□ spring.jpa.open-in-view=false
□ HikariCP pool metrics exposed và alerted
□ Transaction duration metrics exposed và alerted (P99 > threshold)
□ Slow query log enabled (log_min_duration_statement = 100ms)
□ Deadlock detection: pg_stat_activity monitoring
□ Connection pool sized đúng:
  pool_size = (core_count × 2) + 1 per instance
  total = pool_size × number_of_instances < DB max_connections

□ Statement timeout được set (tránh hung transactions):
  spring.jpa.properties.jakarta.persistence.query.timeout=30000

Incident Response Debugging Checklist

Triệu chứng: Application timeout / slow
□ Check hikaricp_connections_pending > 0?
  → Pool exhausted → tìm long transactions
  SELECT pid, now()-xact_start, query FROM pg_stat_activity
  WHERE xact_start IS NOT NULL ORDER BY xact_start;

Triệu chứng: Database CPU cao
□ Check pg_stat_statements cho top CPU consumers
□ Check seq_scan trong pg_stat_user_tables
□ Run EXPLAIN ANALYZE trên slow queries

Triệu chứng: Data inconsistency
□ Check transaction boundaries — là operation atomic?
□ Check rollbackFor — checked exception có rollback không?
□ Check idempotency — có duplicate processing không?
□ Check isolation level — stale read gây inconsistency?

Triệu chứng: Deadlock
□ Check pg_log cho "deadlock detected"
□ Verify lock ordering consistent
□ Tăng lock_timeout và implement retry
□ Reduce transaction scope để giảm lock hold time

Kết luận

Transaction là abstraction giúp bạn viết code “như thể” bạn là user duy nhất của database, trong khi hàng nghìn users khác đang làm tương tự. Nhưng abstraction này có chi phí — lock contention, connection hold time, rollback overhead — và “rò rỉ” khi vượt ra ngoài single database boundary.

Ba nguyên tắc giúp tránh 90% transaction bugs:

1. Transaction scope = database operations only. External calls, network I/O, heavy computation không thuộc vào transaction. Mỗi millisecond trong transaction là một millisecond connection và lock được held.

2. Explicit về rollback behavior. Đừng assume Spring sẽ rollback đúng. Dùng rollbackFor = Exception.class hoặc explicit exceptions. Test rollback paths, không chỉ happy paths.

3. Idempotency là bắt buộc, không phải optional. Bất kỳ operation nào có thể được retry — do network timeout, user click lại, consumer retry — phải safe để gọi nhiều lần. Idempotency key + unique constraint là pattern đơn giản nhất.

Với microservices, chấp nhận rằng distributed consistency phải achieved differently — qua Saga pattern, Outbox pattern, và eventual consistency — thay vì cố làm ACID work across service boundaries. Những pattern đó phức tạp hơn nhưng honest về fundamental trade-offs của distributed systems.

Transaction bug thường không manifest ngay lập tức, không throw exception rõ ràng, và không reproducible easily. Monitoring, observability, và hiểu cơ chế là cách duy nhất để detect và fix chúng trước khi chúng trở thành production incident lúc 2 giờ sáng.