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 DBAi đã 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ì?
Ứ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