Cần hai lớp bảo vệ: một chống trùng đơn (idempotency), một chống bán quá tồn (lock).
Lớp 1 — Idempotency key. Client sinh một key khi mở màn hình checkout và gửi kèm mọi lần submit. Server đặt unique index trên key và tạo đơn theo key đó:
# add_index :orders, :idempotency_key, unique: true
begin
order = Order.create!(idempotency_key: key, user: current_user)
rescue ActiveRecord::RecordNotUnique
order = Order.find_by!(idempotency_key: key) # second click: return the same order
endUnique index ở DB là chốt chặn thật. Chỉ dựa vào validates_uniqueness_of là không đủ — hai request song song đều thấy "chưa tồn tại" rồi cùng insert.
Lớp 2 — Khoá tồn kho khi trừ. Dùng with_lock (bọc SELECT ... FOR UPDATE trong transaction) để hai request nối đuôi nhau thay vì cùng đọc stock = 1:
Product.transaction do
product.with_lock do
raise OutOfStock if product.stock < quantity
product.decrement!(:stock, quantity)
order.line_items.create!(product: product, quantity: quantity)
end
endCác điểm hay bị hỏi thêm:
- Luôn khoá theo cùng một thứ tự (vd tăng dần theo id sản phẩm) khi một đơn có nhiều sản phẩm, nếu không sẽ deadlock.
- Giữ transaction ngắn: không gọi API thanh toán hay gửi mail bên trong khối lock; đẩy ra after_commit hoặc background job.
- Optimistic locking (lock_version) là lựa chọn thay thế khi tranh chấp hiếm — nhẹ hơn, đổi lại phải xử lý StaleObjectError bằng cách thử lại.
- Disable nút ở frontend chỉ là trải nghiệm, không phải cơ chế bảo vệ — request có thể đến từ retry mạng hoặc client khác.