Race Condition Payment

Gặp vấn đề Race Condition trong xử lý thanh toán, bạn sẽ xử lý thế nào ?

Câu hỏi rất hay và rất thực tế – trong xử lý thanh toán, race condition mà xảy ra là mất tiền như chơi, nên bắt buộc phải chắc chắn 100% là thread-safe, không double spending, không oversell.


🎯 Vấn đề thường gặp:

Giả sử user thanh toán đơn hàng (hoặc mua hàng flash sale), backend check số dư tài khoản hoặc tồn kho → thấy đủ → trừ tiền hoặc trừ kho → xác nhận đơn hàng.

Nếu 2 request cùng lúc (multi-thread, multi-instance, retry, v.v.), thì:

  • Cùng lúc đọc số dư / tồn kho

  • Cùng thấy hợp lệ

  • Cùng trừ → gây âm số dư hoặc âm tồn kho


💥 Cách xử lý: (có nhiều cách, tùy vào mức độ yêu cầu)

✅ 1. Optimistic Locking – thường dùng với JPA

  • Dùng annotation @Version trên entity → mỗi lần update, JPA sẽ check version.

  • Nếu version không match (do thread khác vừa update), nó sẽ throw OptimisticLockException.

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

    private BigDecimal balance;

    @Version
    private int version;
}

Trong service:

@Transactional
public void processPayment(Long walletId, BigDecimal amount) {
    Wallet wallet = walletRepository.findById(walletId).orElseThrow();
    
    if (wallet.getBalance().compareTo(amount) < 0) {
        throw new IllegalStateException("Not enough balance");
    }

    wallet.setBalance(wallet.getBalance().subtract(amount));
    walletRepository.save(wallet);
}
  • Nếu có race condition → lần save() thứ hai sẽ fail → ta bắt exception và retry (nếu cần).

  • Ưu điểm: hiệu năng tốt, không khóa cứng dữ liệu.

  • Nhược: cần xử lý retry logic nếu conflict xảy ra.


✅ 2. Pessimistic Locking – khi dữ liệu cực kỳ nhạy cảm

  • Dùng PESSIMISTIC_WRITE để lock row trong DB

  • Ai đã lock thì thread khác phải chờ → tránh race condition tuyệt đối

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT w FROM Wallet w WHERE w.id = :id")
Wallet findByIdForUpdate(@Param("id") Long id);
  • Trong transaction, thread A lock row lại, thread B phải chờ A xong.

  • Ưu điểm: an toàn tuyệt đối.

  • Nhược điểm: cản trở hiệu năng nếu nhiều concurrent request.


✅ 3. Distributed Lock – nếu chạy nhiều instance (clustered system)

  • Sử dụng Redis lock, ví dụ Redisson hoặc tự triển khai qua SETNX + EXPIRE

  • Dùng để khóa logic xử lý của một user (theo userId, orderId...) trên toàn hệ thống

RLock lock = redissonClient.getLock("lock:wallet:" + userId);
try {
    if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
        // process payment safely
    }
} finally {
    lock.unlock();
}
  • Ưu: multi-instance safe, chống race toàn hệ thống

  • Nhược: phức tạp hơn, phải handle lock timeout, deadlock, failover...


✅ 4. Atomic Operation trong DB (best cho high performance)

  • Dùng SQL để xử lý update theo điều kiện và đảm bảo atomic:

UPDATE wallet
SET balance = balance - :amount
WHERE id = :walletId AND balance >= :amount
  • Sau đó check rowsAffected == 1 thì thành công.

  • Nếu 0 → nghĩa là race condition xảy ra hoặc không đủ tiền.

  • Cách này rất nhanh, sử dụng chính DB để đảm bảo atomicity


🧠 Bonus:

  • Không bao giờ nên read rồi mới write nếu data có tính cạnh tranh cao. Luôn prefer:

    • Update with condition

    • Use version

    • Lock row or use distributed locking


✅ Tổng kết – Khi nào dùng gì?

Tình huống
Giải pháp

Ứng dụng đơn máy, load vừa

Optimistic Lock

Tải cao, scale nhiều node

Distributed Lock (Redis) + atomic SQL

Rất nhạy cảm (ví dụ tài chính, inventory flash sale)

Pessimistic Lock hoặc kết hợp cả Lock + Optimistic

Cần hiệu năng cao

SQL Atomic Update (update if condition)


Last updated