Đoạn code dưới đây trông gọn nhưng sai ở hai điểm:
await db.transaction(async (tx) => {
await tx.update(orders).set({ status: 'paid' }).where(eq(orders.id, id))
await paymentGateway.charge(card, amount) // network call inside the transaction
await mailer.send(receipt)
})Vấn đề 1 — giữ khoá theo thời gian mạng. Khoá hàng orders chỉ nhả khi COMMIT. Cổng thanh toán chậm 5 giây thì hàng đó bị giữ 5 giây, kết nối trong pool cũng bị giữ theo. Vài chục request như vậy là cạn pool và toàn hệ thống chờ.
Vấn đề 2 — rollback không gỡ được tác dụng phụ. Nếu mailer.send lỗi, database rollback nhưng thẻ đã bị trừ và mail có thể đã gửi. Transaction chỉ nguyên tử trong phạm vi database.
Cách làm đúng:
- Đưa lệnh gọi ngoài ra khỏi transaction. Transaction chỉ ghi trạng thái, ngắn nhất có thể.
- Cần bảo đảm "ghi DB thành công thì việc phụ chắc chắn chạy" thì dùng transactional outbox: trong cùng transaction, ghi thêm một hàng vào bảng outbox; một worker riêng đọc bảng đó và gọi API, retry cho tới khi thành công.
- Lệnh gọi ngoài phải idempotent (kèm khoá idempotency) vì worker có thể chạy lại.