Dua teller yang melayani nasabah yang sama pada detik yang sama adalah mimpi buruk bank — dan rutinitas biasa bagi database. Isolation level mengatur seberapa “tertutup” sebuah transaksi terhadap transaksi lain yang berjalan bersamaan: transaksi yang sangat terisolasi bekerja seolah sendirian, transaksi yang longgar bisa melihat perubahan orang lain di sela-sela perintahnya. PostgreSQL menyediakan tiga level yang benar-benar berbeda perilakunya — Read Committed, Repeatable Read, dan Serializable — dan memilih level yang salah bisa membuat laporan keuangan berubah angka di tengah pembacaan, atau sebaliknya membuat aplikasi sibuk mengulang transaksi tanpa perlu. Chapter ini memetakan ketiganya, membuktikan yang bisa dibuktikan dari satu sesi, dan memandu eksperimen dua terminal untuk sisanya.
Menyiapkan Tabel Latihan #
Seluruh demo memakai tabel rekening yang sudah akrab dari chapter-chapter sebelumnya, dibuat ulang di database chapter ini supaya contoh tetap mandiri. Tiga rekening dengan saldo tetap akan menjadi bahan bacaan dan bahan tabrakan pada eksperimen isolasi:
CREATE TABLE rekening (
id integer PRIMARY KEY,
nama text NOT NULL,
jenis text NOT NULL,
saldo numeric(12, 2) NOT NULL CHECK (saldo >= 0),
CONSTRAINT saldo_ewallet_maks
CHECK (jenis <> 'e-wallet' OR saldo <= 1000000)
);
INSERT INTO rekening (id, nama, jenis, saldo) VALUES
(1, 'Budi Santoso', 'tabungan', 500000),
(2, 'Siti Aminah', 'tabungan', 300000),
(3, 'Dewi Lestari', 'e-wallet', 950000);
SELECT id, nama, jenis, saldo FROM rekening ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 3
id | nama | jenis | saldo
----+--------------+----------+-----------
1 | Budi Santoso | tabungan | 500000.00
2 | Siti Aminah | tabungan | 300000.00
3 | Dewi Lestari | e-wallet | 950000.00
(3 rows)Level Default dan Cara Membacanya #
Status isolation level tersimpan sebagai dua pengaturan server: transaction_isolation untuk transaksi yang sedang berjalan, dan default_transaction_isolation untuk transaksi-transaksi berikutnya. Keduanya dibaca dengan SHOW:
SHOW TRANSACTION ISOLATION LEVEL;
SHOW default_transaction_isolation;Output yang Dihasilkan:
transaction_isolation
-----------------------
read committed
(1 row)
default_transaction_isolation
-------------------------------
read committed
(1 row)Kedua query melaporkan read committed — level default seluruh instalasi PostgreSQL, kecuali administrator mengubah default_transaction_isolation di postgresql.conf. Level ini berlaku untuk semua transaksi yang tidak mengatur levelnya sendiri, termasuk seluruh demo di chapter-chapter sebelumnya.
Mengatur Level: Dua Bentuk Sintaks #
PostgreSQL menyediakan dua cara menaikkan level untuk satu transaksi. Bentuk pertama menempelkan levelnya langsung di BEGIN; bentuk kedua membuka transaksi polos lalu mengatur levelnya lewat SET TRANSACTION. File berikut memperagakan keduanya, ditutup satu variasi yang menarik — READ UNCOMMITTED, level terendah standar SQL:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SHOW TRANSACTION ISOLATION LEVEL;
COMMIT;
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SHOW TRANSACTION ISOLATION LEVEL;
COMMIT;
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
SHOW TRANSACTION ISOLATION LEVEL;
COMMIT;Output yang Dihasilkan:
BEGIN
transaction_isolation
-----------------------
repeatable read
(1 row)
COMMIT
BEGIN
SET
transaction_isolation
-----------------------
repeatable read
(1 row)
COMMIT
BEGIN
transaction_isolation
-----------------------
read uncommitted
(1 row)
COMMITTiga transaksi berjalan dengan cerita berbeda:
BEGIN ISOLATION LEVEL REPEATABLE READmembuka transaksi yang langsung membaca dengan level repeatable read — bentuk paling ringkas dan paling umum dipakai.SET TRANSACTION ISOLATION LEVEL REPEATABLE READmencapai hasil yang sama lewat perintah terpisah setelahBEGIN.SHOWmelaporkanrepeatable readuntuk keduanya.BEGIN ISOLATION LEVEL READ UNCOMMITTEDditerima dan dilaporkan apa adanya, tetapi perilakunya identik dengan read committed — PostgreSQL tidak pernah mengizinkan transaksi membaca data yang belum di-commit orang lain. Level ini ada semata demi kompatibilitas aplikasi yang memang menuliskannya.
Aturan “harus di awal” itu bukan basa-basi. File berikut melanggarnya dengan sengaja: transaksi dibuka, saldo dibaca dulu, baru SET TRANSACTION dipanggil:
BEGIN;
SELECT saldo FROM rekening WHERE id = 1;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;Output yang Dihasilkan:
psql:set-transaction-terlambat.sql:5: ERROR: SET TRANSACTION ISOLATION LEVEL must be called before any queryPostgreSQL menolak perintah ketiga dengan pesan SET TRANSACTION ISOLATION LEVEL must be called before any query. Alasannya teknis: snapshot transaksi — potongan pandangan data yang menentukan apa yang bisa dilihat — sudah diambil saat query pertama berjalan, dan mengganti level di tengah jalan berarti mengganti aturan main setelah kartu dibagikan. Kalau level perlu diatur, atur sejak BEGIN.
Tiga Level dan Fenomena yang Dicegah #
Standar SQL mendefinisikan isolation level lewat fenomena aneh yang boleh atau tidak boleh terjadi ketika dua transaksi berjalan bersamaan. Empat fenomenanya, diterjemahkan ke dompet rekening kita:
- Dirty read — membaca perubahan milik transaksi lain yang belum di-commit (dan masih bisa dibatalkan). Contoh: teller B melihat saldo “Rp450.000” hasil transfer yang sedang berjalan, lalu transfer itu di-rollback.
- Non-repeatable read — satu baris yang sama terbaca dua kali dalam satu transaksi dengan hasil berbeda. Contoh: transaksi laporan membaca saldo Budi Rp500.000, transaksi lain mengubahnya dan commit, laporan membaca lagi dan mendapat Rp550.000.
- Phantom read — query yang sama dijalankan dua kali dan muncul baris baru yang sebelumnya tidak ada. Contoh: query menghitung mutasi hari ini menghasilkan 10 baris, transaksi lain menambah mutasi baru dan commit, query diulang dan mendapat 11 baris.
- Serialization anomaly — hasil sekelompok transaksi bersamaan tidak bisa diputar ulang seolah mereka dijalankan satu per satu. Contoh klasik: dua teller sama-sama membaca saldo Rp500.000, sama-sama menyimpulkan “masih ada ruang”, dan keduanya mengirim uang — totalnya melebihi saldo yang seharusnya dijaga.
Pemetaan level ke fenomena di PostgreSQL dirangkum tabel berikut:
| Fenomena | Read Committed | Repeatable Read | Serializable |
|---|---|---|---|
| Dirty read | mustahil | mustahil | mustahil |
| Non-repeatable read | bisa terjadi | dicegah | dicegah |
| Phantom read | bisa terjadi | dicegah | dicegah |
| Serialization anomaly | bisa terjadi | bisa terjadi | dicegah |
Dua hal langsung terlihat dari tabel ini. Pertama, kolom dirty read selalu “mustahil” — PostgreSQL menolak membaca data yang belum di-commit di level apa pun. Kedua, repeatable read di PostgreSQL lebih kuat dari standar SQL minimum: phantom read ikut dicegah berkat snapshot yang diambil sekali di awal transaksi, bukan per pernyataan. Serializable adalah level paling ketat — mendeteksi bahkan serialization anomaly — dengan konsekuensi transaksi kadang harus diulang; pola pemakaiannya dibahas pada chapter Serializable Transactions di section ini.
Snapshot dalam Praktik Satu Sesi #
Repeatable Read bekerja lewat snapshot: potongan pandangan data yang dibekukan di awal transaksi. Yang bisa dibuktikan dari satu sesi adalah aturan pelengkapnya — perubahan buatan transaksi sendiri tetap terlihat di dalam snapshot itu, dan semuanya hilang begitu di-rollback:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT id, nama, saldo FROM rekening WHERE id = 1;
UPDATE rekening SET saldo = saldo - 1000 WHERE id = 1;
SELECT id, nama, saldo FROM rekening WHERE id = 1;
ROLLBACK;
SELECT id, nama, saldo FROM rekening WHERE id = 1;Output yang Dihasilkan:
BEGIN
id | nama | saldo
----+--------------+-----------
1 | Budi Santoso | 500000.00
(1 row)
UPDATE 1
id | nama | saldo
----+--------------+-----------
1 | Budi Santoso | 499000.00
(1 row)
ROLLBACK
id | nama | saldo
----+--------------+-----------
1 | Budi Santoso | 500000.00
(1 row)Saldo Budi terbaca Rp500.000 di awal, Rp499.000 setelah UPDATE milik transaksi itu sendiri, lalu kembali Rp500.000 setelah ROLLBACK. Perubahan sendiri selalu terlihat — snapshot bukan foto yang membekukan milik Anda sendiri. Justru di sinilah batas demo satu sesi: untuk membuktikan bahwa perubahan sesi lain tidak bocor masuk ke snapshot, kita butuh sesi kedua yang sungguh-sungguh berjalan bersamaan.
Eksperimen Dua Terminal #
Fenomena isolasi hanya hidup di konkurensi nyata, dan cara paling jujur membuktikannya adalah membuka dua terminal psql sekaligus. Eksperimen berikut mendemokan non-repeatable read di level default, lalu hilangnya fenomena itu di repeatable read. Jalankan langkah-langkah ini di database latihan Anda:
- Buka dua terminal dan jalankan
psqldi keduanya, terhubung ke database yang sama. Sebut saja terminal A dan terminal B. - Di terminal A, buka transaksi level default lalu baca saldo Budi: jalankan
BEGIN;laluSELECT saldo FROM rekening WHERE id = 1;— hasilnya Rp500.000. Biarkan transaksi ini terbuka. - Di terminal B, ubah saldo yang sama dan commit seketika:
BEGIN; UPDATE rekening SET saldo = saldo + 50000 WHERE id = 1; COMMIT; - Kembali ke terminal A, baca ulang saldo yang sama:
SELECT saldo FROM rekening WHERE id = 1;— di level read committed hasilnya sekarang Rp550.000. Transaksi A belum selesai, tetapi angkanya sudah berubah di tengah jalan: itulah non-repeatable read dari tabel di atas. Tutup denganCOMMIT;lalu di terminal B jalankanUPDATE rekening SET saldo = saldo - 50000 WHERE id = 1;untuk mengembalikan saldo ke Rp500.000 sebelum percobaan kedua. - Ulangi eksperimen, tetapi di langkah 2 ganti pembukaannya menjadi
BEGIN ISOLATION LEVEL REPEATABLE READ;— SELECT kedua di terminal A kini tetap melaporkan Rp500.000 meski terminal B sudah commit: snapshot transaksi A dibekukan di awal. - Masih pada percobaan kedua itu, coba terminal A mengubah baris yang sama:
UPDATE rekening SET saldo = saldo - 10000 WHERE id = 1;— PostgreSQL menolaknya dengan errorcould not serialize access due to concurrent update, karena baris itu sudah berubah di luar snapshot A. Akhiri denganROLLBACK;lalu pulihkan saldo lewat terminal B denganUPDATE rekening SET saldo = saldo - 50000 WHERE id = 1;— kembali ke Rp500.000.
Hasil akhir percobaan kedua mencerminkan filosofi repeatable read: lebih baik menolak perubahan yang bertabrakan dengan snapshot daripada membiarkan transaksi bekerja di atas data yang sudah basi. Aplikasi tanggap menanggapi error semacam ini dengan membaca ulang data dan mencoba lagi — pola retry yang sama yang menjadi inti pembahasan Serializable.
Semua perilaku di atas — snapshot, versi baris, pembacaan tanpa saling memblok — bukan sihir: PostgreSQL menyimpan beberapa versi tiap baris dan setiap transaksi memilih versi yang cocok dengan snapshotnya. Mekanisme itu bernama MVCC, dan membedahnya adalah urusan chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkasCREATE TABLE rekening (
id integer PRIMARY KEY,
nama text NOT NULL,
jenis text NOT NULL,
saldo numeric(12, 2) NOT NULL CHECK (saldo >= 0),
CONSTRAINT saldo_ewallet_maks
CHECK (jenis <> 'e-wallet' OR saldo <= 1000000)
);
INSERT INTO rekening (id, nama, jenis, saldo) VALUES
(1, 'Budi Santoso', 'tabungan', 500000),
(2, 'Siti Aminah', 'tabungan', 300000),
(3, 'Dewi Lestari', 'e-wallet', 950000);
SELECT id, nama, jenis, saldo FROM rekening ORDER BY id;
level-saat-ini.sql
Unduh berkasSHOW TRANSACTION ISOLATION LEVEL;
SHOW default_transaction_isolation;
set-level.sql
Unduh berkasBEGIN ISOLATION LEVEL REPEATABLE READ;
SHOW TRANSACTION ISOLATION LEVEL;
COMMIT;
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SHOW TRANSACTION ISOLATION LEVEL;
COMMIT;
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
SHOW TRANSACTION ISOLATION LEVEL;
COMMIT;
set-transaction-terlambat.sql
Unduh berkasBEGIN;
SELECT saldo FROM rekening WHERE id = 1;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
snapshot-sesi-tunggal.sql
Unduh berkasBEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT id, nama, saldo FROM rekening WHERE id = 1;
UPDATE rekening SET saldo = saldo - 1000 WHERE id = 1;
SELECT id, nama, saldo FROM rekening WHERE id = 1;
ROLLBACK;
SELECT id, nama, saldo FROM rekening WHERE id = 1;