Lompat ke konten
19 menit bacamateri 29 dari 96

Reservasi stok terbatas

Modelkan hold, kedaluwarsa, konfirmasi, pembatalan, dan klaim bersamaan supaya barang terakhir nggak terjual dua kali.

Ditinjau 26 Oktober 2026

Pertanyaan

Bagaimana menahan barang terakhir untuk satu pembeli tanpa menjualnya dua kali atau menguncinya selamanya?

Setelah baca ini, kamu bisa menggambar siklus hidup sebuah reservasi, menulis klaim atomik untuk stok terbatas, dan menjelaskan apa yang terjadi saat hold kedaluwarsa, dibatalkan, atau dikonfirmasi dua kali.

Prasyarat dan contoh

Kamu sudah paham invarians (M05.01), pembaruan atomik dan retry (M05.03), dan identifier (M05.04).

Flash sale: 500 unit, 20.000 pembeli dalam 10 detik pertama, kira-kira 2.000 klaim/detik pada satu produk. Pembeli butuh waktu hingga 10 menit untuk membayar. Dua kegagalan klasik: menjual unit ke-501 (oversell), atau menahan semua unit oleh pembeli yang nggak pernah membayar sehingga produk "habis" padahal nggak terjual.

Mekanisme

Model dengan tiga hitungan. Per produk simpan total, ditahan (hold aktif), dan terjual. Yang tersedia: total - ditahan - terjual. Invarians: ditahan + terjual <= total.

Siklus hidup reservasi.

DITAHAN --bayar--> DIKONFIRMASI
   |  \--batal---> DIBATALKAN  (stok kembali)
   \---lewat batas waktu---> KEDALUWARSA  (stok kembali)

Tiap reservasi punya id, produk, pembeli, status, dan kedaluwarsa_pada.

Klaim atomik. Satu pernyataan menahan stok bila tersedia:

UPDATE stok SET ditahan = ditahan + :n
WHERE produk = 7 AND total - ditahan - terjual >= :n;
-- 1 baris = berhasil, lanjut INSERT reservasi dalam transaksi yang sama
-- 0 baris = habis

Pengecekan dan penambahan terjadi dalam satu langkah di bawah kunci baris, jadi 2.000 klaim/detik berurutan di baris itu tanpa oversell. Inilah biaya yang diterima: satu baris produk menjadi titik serial. Kalau satu baris nggak sanggup, pecah stok jadi beberapa "keranjang" (misalnya 10 baris masing-masing 50 unit) dan klaim ke satu keranjang yang masih punya sisa. Invarians tetap dijaga per keranjang.

Konfirmasi dengan transisi bersyarat.

UPDATE reservasi SET status = 'DIKONFIRMASI'
WHERE id = :rid AND status = 'DITAHAN' AND kedaluwarsa_pada > now();

Lalu pindahkan hitungan: ditahan - 1, terjual + 1, di transaksi yang sama. Jika 0 baris terpengaruh, hold sudah kedaluwarsa atau dibatalkan, dan pembayaran harus dikembalikan atau ditangani.

Kedaluwarsa. Dua pendekatan yang bisa digabung:

  • Lazy: hitung hold yang kedaluwarsa_pada sudah lewat sebagai tersedia saat menghitung stok. Nggak butuh pekerjaan latar, tapi hitungan jadi query agregat.
  • Sweeper: pekerjaan periodik (misalnya tiap 10 detik) mengubah reservasi lewat batas jadi KEDALUWARSA dan mengurangi ditahan, per batch (misalnya 500 baris) dengan transisi bersyarat WHERE status = 'DITAHAN' agar aman dijalankan dua kali.

Retry dan idempotensi. Klien yang mengulang "tahan 1 unit" karena timeout jangan menahan dua unit. Kirim kunci_idempoten (misalnya UUID per percobaan checkout) dengan UNIQUE pada (pembeli, kunci_idempoten). Ulangan mengembalikan reservasi yang sama.

Pembatalan. Mirip kedaluwarsa: transisi DITAHAN -> DIBATALKAN bersyarat, lalu ditahan - 1. Pembatalan setelah DIKONFIRMASI adalah proses lain (refund), bukan pelepasan hold.

Saat rancangan ini gagal

  • Cek dulu, tahan kemudian. SELECT stok lalu UPDATE tanpa syarat di WHERE menjual melebihi stok.
  • Hold tanpa batas waktu. Bot yang menahan 500 unit dan nggak pernah membayar menghabiskan stok. Batasi waktu hold (misalnya 5 sampai 10 menit) dan jumlah hold per pembeli.
  • Sweeper tidak aman diulang. Dua instance sweeper yang mengurangi ditahan untuk reservasi yang sama membuat hitungan jadi tidak benar. Selalu transisi bersyarat.
  • Konfirmasi setelah hold lewat. Pembeli membayar pada menit ke-11. Kebijakan harus eksplisit: tolak dan refund, atau terima bila stok masih ada. Jangan dibiarkan sebagai kebetulan.
  • Satu baris panas. Pada beban jauh di atas ribuan klaim/detik per produk, antrean di kunci baris menaikkan latensi. Ukur lalu pecah ke keranjang atau pakai antrean di depan.
  • Menganggap cache stok sebagai kebenaran. Tampilan "sisa 3" boleh usang. Keputusan klaim tidak boleh.

Latihan

Konser punya 1 kursi tersisa. Pembeli A menekan "pesan" pada detik 0 dan memegang hold 5 menit. Pembeli B menekan "pesan" pada detik 2. Pada menit ke-5 lewat 3 detik, A membayar dan di saat yang sama sweeper berjalan. B mencoba lagi pada menit ke-5 lewat 2 detik.

Jelaskan apa yang harus terjadi dan transisi mana yang menjamin hasilnya.

Lihat pembahasan

Detik 0: klaim A berhasil (total - ditahan - terjual = 1 >= 1), ditahan = 1. Detik 2: klaim B mendapat 0 baris (tersedia 0), B diberi "habis, kami kabari kalau dilepas".

Menit ke-5 lewat 2 detik: hold A sudah lewat batas tapi sweeper belum jalan. Dengan pendekatan lazy, kursi dihitung tersedia sehingga klaim B berhasil, dan ditahan dikoreksi oleh sweeper untuk reservasi A. Dengan hanya sweeper, B ditolak sampai sweeper berjalan.

Menit ke-5 lewat 3 detik: A mencoba konfirmasi dan sweeper menandai A kedaluwarsa bersamaan. Keduanya UPDATE reservasi ... WHERE status = 'DITAHAN'. Yang menang menentukan hasil:

  • Konfirmasi A menang hanya bila kedaluwarsa_pada > now() masih benar. Karena sudah lewat, konfirmasi mendapat 0 baris, A ditolak dan dananya dikembalikan.
  • Sweeper menang: status KEDALUWARSA, ditahan - 1.

Kursi tidak pernah dimiliki dua orang karena kursi hanya berpindah lewat klaim atomik dan setiap transisi status bersyarat. Kebijakan yang perlu ditulis eksplisit: A yang membayar terlambat ditolak, bukan "mungkin diterima".

Terkait

Catatan

  • Asumsi: satu database relasional sebagai sumber kebenaran stok. Replikasi dan failover (M08) mengubah jaminannya dan nggak dibahas di sini.
  • Angka 2.000 klaim/detik dan 10 menit adalah contoh. Ukur kapasitas baris panas di sistemmu.
  • Yang nggak dibahas: ruang tunggu virtual, anti-bot, dan penetapan harga.
  • Terakhir ditinjau: 26 Oktober 2026.

Isi · System Design