Kodeloka
  1. Kodeloka
  2. Kelas PostgreSQL
  3. Materi

4 · Transaksi · Modul 5 dari 12

Transaksi, Tingkat Isolasi & Deadlock

Apa yang benar-benar dijamin tiap tingkat isolasi, kenapa deadlock terjadi, dan cara mengunci baris tanpa mematikan konkurensi.

Tingkat Isolasi

TingkatDirty readNon-repeatable readPhantom readAnomali serialisasi
Read Committed (bawaan)TidakMungkinMungkinMungkin
Repeatable ReadTidakTidakTidakMungkin
SerializableTidakTidakTidakTidak
💡 PostgreSQL tidak punya Read Uncommitted sungguhan

Karena MVCC, PostgreSQL tidak pernah membaca data yang belum di-commit. Meminta Read Uncommitted akan diperlakukan sebagai Read Committed. Perhatikan juga bahwa Repeatable Read di PostgreSQL sudah mencegah phantom read — lebih kuat daripada yang diwajibkan standar SQL.

Jebakan Read Committed

-- Setiap PERNYATAAN melihat potret terbaru, bukan potret transaksi
BEGIN;
SELECT stok FROM produk WHERE id = 1;    -- 10
-- transaksi lain melakukan UPDATE dan COMMIT
SELECT stok FROM produk WHERE id = 1;    -- 5  ← BERUBAH!
COMMIT;
🚫 Pola baca-lalu-tulis yang rusak
-- ❌ Ada celah balapan
SELECT stok FROM produk WHERE id = 1;         -- aplikasi membaca 1
-- transaksi lain juga membaca 1 di detik yang sama
UPDATE produk SET stok = 0 WHERE id = 1;      -- keduanya menjual!

-- ✅ Biarkan basis data yang menghitung
UPDATE produk SET stok = stok - 1
WHERE id = 1 AND stok >= 1
RETURNING stok;
-- Bila tidak ada baris yang dikembalikan, stok memang habis.

Penguncian Eksplisit

-- Kunci baris; transaksi lain MENUNGGU
SELECT * FROM produk WHERE id = 1 FOR UPDATE;

-- Jangan menunggu, langsung gagal
SELECT * FROM produk WHERE id = 1 FOR UPDATE NOWAIT;

-- Lewati baris yang sedang dikunci — POLA ANTREAN
SELECT * FROM tugas
WHERE status = 'menunggu'
ORDER BY dibuat_pada
LIMIT 10
FOR UPDATE SKIP LOCKED;
📌 SKIP LOCKED membuat PostgreSQL menjadi antrean yang layak

Dengan FOR UPDATE SKIP LOCKED, sepuluh pekerja bisa mengambil pekerjaan dari tabel yang sama tanpa saling menunggu — masing-masing melewati baris yang sudah dipegang pekerja lain. Untuk beban kerja menengah, ini sering menghilangkan kebutuhan akan broker pesan terpisah, sekaligus memberi Anda transaksi yang sama dengan data bisnis.

Deadlock

Transaksi A                    Transaksi B
UPDATE akun SET .. WHERE id=1  UPDATE akun SET .. WHERE id=2
                               UPDATE akun SET .. WHERE id=1  ← tunggu A
UPDATE akun SET .. WHERE id=2  ← tunggu B
                    DEADLOCK

PostgreSQL mendeteksinya setelah deadlock_timeout (bawaan 1 detik) dan membatalkan salah satu transaksi dengan SQLSTATE 40P01.

💡 Pencegahan yang paling efektif

Selalu kunci sumber daya dalam urutan yang konsisten. Bila setiap transaksi mengunci akun dengan ID menaik, deadlock secara struktural tidak mungkin terjadi:

SELECT * FROM akun
WHERE id IN (LEAST(a, b), GREATEST(a, b))
ORDER BY id
FOR UPDATE;

Aplikasi juga harus siap mengulang transaksi yang gagal karena deadlock — ini kondisi sementara yang normal, bukan bug.

Serializable untuk Invarian yang Rumit

BEGIN ISOLATION LEVEL SERIALIZABLE;
-- Aturan bisnis: minimal 1 dokter harus tetap berjaga
SELECT count(*) FROM jadwal WHERE berjaga = true;   -- 2
UPDATE jadwal SET berjaga = false WHERE dokter = 'Ani';
COMMIT;
-- Bila dua dokter mencoba keluar jaga bersamaan,
-- salah satunya GAGAL dengan SQLSTATE 40001.
⚠️ Serializable mewajibkan logika retry

Tingkat ini mendeteksi konflik dan membatalkan transaksi dengan serialization_failure. Ini bukan kesalahan aplikasi — ini cara kerjanya. Kode Anda wajib menangkap SQLSTATE 40001 dan mengulang seluruh transaksi. Tanpa itu, pengguna akan melihat error acak saat trafik ramai.

Melihat dan Mengakhiri Kunci

-- Siapa memblokir siapa
SELECT
    dibl.pid AS pid_diblokir,
    left(dibl.query, 50) AS kueri_diblokir,
    pembl.pid AS pid_pemblokir,
    left(pembl.query, 50) AS kueri_pemblokir
FROM pg_stat_activity dibl
JOIN pg_stat_activity pembl
  ON pembl.pid = ANY(pg_blocking_pids(dibl.pid))
WHERE cardinality(pg_blocking_pids(dibl.pid)) > 0;

SELECT pg_cancel_backend(12345);      -- batalkan kueri (halus)
SELECT pg_terminate_backend(12345);   -- putus koneksi (keras)
🚫 Transaksi menganggur adalah pembunuh senyap

Koneksi berstatus idle in transaction menahan kunci dan mencegah VACUUM membersihkan dead tuple di seluruh basis data. Satu transaksi yang lupa di-commit karena bug aplikasi dapat membuat tabel membengkak berhari-hari. Selalu pasang pengaman:

ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
ALTER SYSTEM SET statement_timeout = '30s';
ALTER SYSTEM SET lock_timeout = '10s';