Tingkat Isolasi
| Tingkat | Dirty read | Non-repeatable read | Phantom read | Anomali serialisasi |
|---|---|---|---|---|
| Read Committed (bawaan) | Tidak | Mungkin | Mungkin | Mungkin |
| Repeatable Read | Tidak | Tidak | Tidak | Mungkin |
| Serializable | Tidak | Tidak | Tidak | Tidak |
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;
-- ❌ 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;
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.
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.
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)
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';