Koordinasikan perubahan konkuren
Bandingkan row lock, konkurensi optimistis, pembaruan atomik, dan cara menangani deadlock.
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
40P01dan 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 UPDATEpada 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
- M05.02 Amati anomali isolasi menunjukkan anomali yang diperbaiki alat-alat di sini.
- M05.05 Reservasi stok terbatas memakai pembaruan atomik untuk klaim stok.
- M10 membahas retry dan idempotensi di sisi antrean.
Catatan
- Asumsi: PostgreSQL.
SELECT ... FOR UPDATE SKIP LOCKEDada 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.