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:

sql buat-tabel.sql
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:

text
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:

text
 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:

sql set-level.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:

text
BEGIN
 transaction_isolation 
-----------------------
 repeatable read
(1 row)

COMMIT
BEGIN
SET
 transaction_isolation 
-----------------------
 repeatable read
(1 row)

COMMIT
BEGIN
 transaction_isolation 
-----------------------
 read uncommitted
(1 row)

COMMIT

Tiga transaksi berjalan dengan cerita berbeda:

  1. BEGIN ISOLATION LEVEL REPEATABLE READ membuka transaksi yang langsung membaca dengan level repeatable read — bentuk paling ringkas dan paling umum dipakai.
  2. SET TRANSACTION ISOLATION LEVEL REPEATABLE READ mencapai hasil yang sama lewat perintah terpisah setelah BEGIN. SHOW melaporkan repeatable read untuk keduanya.
  3. BEGIN ISOLATION LEVEL READ UNCOMMITTED diterima 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:

text
psql:set-transaction-terlambat.sql:5: ERROR:  SET TRANSACTION ISOLATION LEVEL must be called before any query

PostgreSQL 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:

FenomenaRead CommittedRepeatable ReadSerializable
Dirty readmustahilmustahilmustahil
Non-repeatable readbisa terjadidicegahdicegah
Phantom readbisa terjadidicegahdicegah
Serialization anomalybisa terjadibisa terjadidicegah

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:

text
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:

  1. Buka dua terminal dan jalankan psql di keduanya, terhubung ke database yang sama. Sebut saja terminal A dan terminal B.
  2. Di terminal A, buka transaksi level default lalu baca saldo Budi: jalankan BEGIN; lalu SELECT saldo FROM rekening WHERE id = 1; — hasilnya Rp500.000. Biarkan transaksi ini terbuka.
  3. Di terminal B, ubah saldo yang sama dan commit seketika: BEGIN; UPDATE rekening SET saldo = saldo + 50000 WHERE id = 1; COMMIT;
  4. 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 dengan COMMIT; lalu di terminal B jalankan UPDATE rekening SET saldo = saldo - 50000 WHERE id = 1; untuk mengembalikan saldo ke Rp500.000 sebelum percobaan kedua.
  5. 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.
  6. Masih pada percobaan kedua itu, coba terminal A mengubah baris yang sama: UPDATE rekening SET saldo = saldo - 10000 WHERE id = 1; — PostgreSQL menolaknya dengan error could not serialize access due to concurrent update, karena baris itu sudah berubah di luar snapshot A. Akhiri dengan ROLLBACK; lalu pulihkan saldo lewat terminal B dengan UPDATE 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 berkas
sql
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;

level-saat-ini.sql

Unduh berkas
sql
SHOW TRANSACTION ISOLATION LEVEL;

SHOW default_transaction_isolation;

set-level.sql

Unduh berkas
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;

set-transaction-terlambat.sql

Unduh berkas
sql
BEGIN;

SELECT saldo FROM rekening WHERE id = 1;

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

snapshot-sesi-tunggal.sql

Unduh berkas
sql
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;