Lompat ke konten
19 menit bacamateri 27 dari 96

Koordinasikan perubahan konkuren

Bandingkan row lock, konkurensi optimistis, pembaruan atomik, dan cara menangani deadlock.

Ditinjau 26 Oktober 2026

Pertanyaan

Bagaimana dua penulis bisa mengubah data yang sama tanpa saling menimpa, dan berapa biayanya?

Setelah baca ini, kamu bisa memilih antara pembaruan atomik, row lock, dan versi optimistis untuk sebuah perubahan, serta menjelaskan bagaimana deadlock muncul dan ditangani.

Prasyarat dan contoh

Kamu sudah melihat lost update dan write skew di M05.02.

Stok sebuah produk flash sale: 1 unit tersisa, 300 permintaan/detik masuk. Setiap permintaan harus mengurangi stok dan membuat pesanan, dan stok nggak boleh di bawah nol. Ada tiga cara standar untuk menjaganya, dengan biaya yang berbeda.

Mekanisme

1. Pembaruan atomik bersyarat. Biarkan database menghitung dan mengecek dalam satu pernyataan:

UPDATE stok SET sisa = sisa - 1 WHERE produk = 7 AND sisa > 0;
-- cek jumlah baris terpengaruh: 1 = berhasil, 0 = habis

Database mengunci baris selama pernyataan berjalan, jadi nggak ada jendela antara baca dan tulis. Ini opsi termurah dan paling sering cukup untuk satu baris. Batasnya: logika harus muat dalam satu pernyataan.

2. Row lock eksplisit. SELECT ... FOR UPDATE mengunci baris sampai commit. Transaksi lain yang menyentuh baris itu menunggu.

BEGIN;
SELECT sisa FROM stok WHERE produk = 7 FOR UPDATE;
-- logika aplikasi, boleh kompleks
UPDATE stok SET sisa = sisa - 1 WHERE produk = 7;
COMMIT;

Cocok kalau keputusan butuh beberapa langkah atau beberapa baris. Biayanya: antrean. Kalau transaksi menahan kunci 20 ms dan tiap permintaan butuh baris yang sama, throughput maksimal sekitar 1 / 0,02 = 50 transaksi/detik untuk baris itu. Jangan memanggil layanan eksternal sambil memegang kunci.

3. Konkurensi optimistis. Simpan kolom versi. Baca, lalu tulis dengan syarat versi belum berubah.

UPDATE dokumen SET isi = :baru, versi = versi + 1
WHERE id = 3 AND versi = :versi_terbaca;
-- 0 baris terpengaruh = ada yang menulis lebih dulu: baca ulang, putuskan, coba lagi

Tanpa kunci selama pengguna berpikir (misalnya mengedit formulir 3 menit). Murah saat konflik jarang. Saat konflik sering, banyak percobaan terbuang dan pengguna melihat "data berubah, ulangi".

Deadlock. T1 mengunci baris A lalu menunggu B. T2 mengunci B lalu menunggu A. Keduanya menunggu selamanya. Database mendeteksinya (PostgreSQL mengecek setelah deadlock_timeout, default 1 detik) dan membatalkan salah satu dengan error 40P01.

T1: lock akun 1    T2: lock akun 2
T1: lock akun 2 (menunggu)    T2: lock akun 1 (menunggu)  -- deadlock

Pencegahan yang paling efektif: urutan kunci yang konsisten. Transfer antar akun selalu mengunci akun dengan id lebih kecil dulu. Selain itu, jaga transaksi tetap pendek dan retry transaksi yang dibatalkan.

Pesimistis vs optimistis, ringkas:

| | Pembaruan atomik | Row lock | Versi optimistis | | --- | --- | --- | --- | | Menunggu | sangat singkat | selama transaksi | tidak ada | | Risiko | hanya satu baris | deadlock, antrean | retry saat konflik | | Cocok untuk | counter, stok | alur beberapa langkah | edit panjang |

Saat rancangan ini gagal

  • Kunci dipegang terlalu lama. Panggilan HTTP 300 ms ke penyedia pembayaran di dalam transaksi yang mengunci baris stok membatasi baris itu ke sekitar 3 transaksi/detik.
  • Lupa retry deadlock dan konflik versi. Error 40P01 dan 0 baris terpengaruh adalah hasil normal yang harus diulang, dengan batas percobaan dan sedikit jitter.
  • Retry tanpa batas saat konflik tinggi. Seribu klien yang terus mengulang pada baris yang sama memperparah kontensi. Batasi dan kembalikan "coba lagi nanti".
  • Urutan kunci berbeda antar jalur kode. Satu jalur mengunci pesanan lalu stok, jalur lain stok lalu pesanan. Deadlock muncul hanya saat beban tinggi.
  • Mengunci baris yang salah. FOR UPDATE pada baris pelanggan nggak mencegah dua orang berbeda mengambil stok yang sama. Kunci harus pada sumber daya yang dipertaruhkan.

Latihan

Dua pekerjaan jalan bersamaan. Pekerjaan X memindahkan saldo dari akun 5 ke akun 9. Pekerjaan Y memindahkan dari akun 9 ke akun 5. Masing-masing: FOR UPDATE akun asal, lalu FOR UPDATE akun tujuan, lalu dua UPDATE.

Jelaskan apa yang bisa terjadi dan perbaiki tanpa mengubah logika transfer.

Lihat pembahasan

X mengunci akun 5 lalu meminta 9. Y mengunci akun 9 lalu meminta 5. Keduanya menunggu: deadlock. PostgreSQL membatalkan salah satunya dengan 40P01 setelah sekitar 1 detik, jadi pengguna melihat transfer gagal atau lambat.

Perbaikan: ubah urutan penguncian menjadi berdasarkan id, apa pun arah transfernya.

SELECT ... FROM akun WHERE id IN (5, 9) ORDER BY id FOR UPDATE;

Sekarang X dan Y sama-sama mengunci 5 dulu. Yang kalah menunggu sampai yang menang commit, lalu lanjut. Nggak ada siklus tunggu, jadi nggak ada deadlock di jalur ini.

Tetap tambahkan retry untuk 40P01 karena jalur kode lain masih bisa menimbulkannya, dan cek CHECK (saldo >= 0) sebagai jaring terakhir.

Terkait

Catatan

  • Asumsi: PostgreSQL. SELECT ... FOR UPDATE SKIP LOCKED ada tapi nggak dibahas di sini.
  • Yang nggak dibahas: lock terdistribusi dan lease (M08), advisory lock.
  • Angka throughput adalah batas atas kasar untuk satu baris panas, bukan hasil benchmark.
  • Terakhir ditinjau: 26 Oktober 2026.

Isi · System Design