Jaminan Isolation yang kita kenal pada Chapter Transaksi dan ACID mudah dipenuhi saat sebuah transaksi berjalan sendirian. Kenyataannya, database toko online melayani puluhan transaksi dalam waktu yang sama: satu koneksi memotong stok Kemeja Batik, koneksi lain membaca sisa stok, dan koneksi ketiga menjalankan laporan penjualan. Isolation level adalah pengaturan yang menentukan seberapa banyak gangguan antar transaksi yang diperbolehkan — semakin longgar levelnya, semakin besar peluang data yang dibaca menjadi tidak konsisten.

Tiga Anomali Konkurensi #

Ketika transaksi berjalan bersamaan tanpa isolasi, tiga anomali klasik dapat muncul:

  • Dirty read — transaksi A membaca perubahan transaksi B yang belum di-COMMIT. Bila B kemudian ROLLBACK, nilai yang dibaca A tidak pernah benar-benar ada; laporan stok bisa menampilkan 9 padahal nilai sah tetap 10.
  • Non-repeatable read — transaksi A membaca satu baris dua kali, dan hasilnya berbeda karena transaksi B mengubah lalu meng-commit baris tersebut di antara dua pembacaan itu.
  • Phantom read — transaksi A menjalankan query pencarian dua kali, dan kumpulan baris hasilnya berubah karena transaksi B menyisipkan atau menghapus baris yang cocok dengan kriteria pencarian.

Empat Standar Isolation Level #

Standar SQL mendefinisikan empat level yang menumpuk — setiap level yang lebih ketat mencegah anomali level di bawahnya.

Isolation levelDirty readNon-repeatable readPhantom read
READ UNCOMMITTEDmungkin terjadimungkin terjadimungkin terjadi
READ COMMITTEDdicegahmungkin terjadimungkin terjadi
REPEATABLE READdicegahdicegahmungkin terjadi
SERIALIZABLEdicegahdicegahdicegah

Level yang lebih ketat memberi hasil lebih konsisten tetapi membatasi ruang gerak transaksi yang berjalan bersamaan, sehingga throughput dapat turun.

Default di SQLite dan PostgreSQL #

SQLite pada mode bawaan (rollback journal) memberikan isolasi setara SERIALIZABLE dengan menyerialisasi penulisannya: hanya satu penulis yang aktif pada satu waktu, dan pembaca tidak pernah melihat perubahan yang belum di-commit. Konsekuensinya, dua transaksi tulis yang berpapasan berujung pada galat database is locked alih-alih anomali data.

PostgreSQL lebih fleksibel. Level bawaannya adalah READ COMMITTED: setiap statement melihat data yang sudah di-commit saat statement tersebut dimulai, sehingga perubahan dari transaksi lain yang selesai di tengah jalan terlihat pada statement berikutnya. Aplikasi dapat menaikkan level untuk satu transaksi dengan SET TRANSACTION ISOLATION LEVEL SERIALIZABLE sebelum statement pertamanya.

Simulasi Race dengan Python Threading #

Mari kita buktikan perilaku SQLite secara nyata. Dua koneksi sama-sama memotong stok Kemeja Batik yang tersisa 10 unit: penulis pertama menahan transaksi tulis selama satu detik, penulis kedua datang 0,3 detik kemudian — sekali dengan timeout=0 (tidak mau menunggu), sekali dengan timeout=5.

python
import sqlite3
import threading
import time
from pathlib import Path

DB = Path("toko.db")


def siapkan_db():
    if DB.exists():
        DB.unlink()
    koneksi = sqlite3.connect(DB)
    koneksi.execute("CREATE TABLE stok (produk TEXT PRIMARY KEY, jumlah INTEGER)")
    koneksi.execute("INSERT INTO stok VALUES ('Kemeja Batik', 10)")
    koneksi.commit()
    koneksi.close()


def penulis_pertama():
    koneksi = sqlite3.connect(DB, timeout=5, isolation_level=None)
    koneksi.execute("BEGIN")
    koneksi.execute("UPDATE stok SET jumlah = jumlah - 1")
    print("[penulis-1] UPDATE tercatat, menahan kunci tulis 1 detik")
    time.sleep(1.0)
    koneksi.execute("COMMIT")
    print("[penulis-1] COMMIT berhasil")
    koneksi.close()


def penulis_kedua(timeout):
    time.sleep(0.3)
    koneksi = sqlite3.connect(DB, timeout=timeout, isolation_level=None)
    try:
        koneksi.execute("BEGIN")
        koneksi.execute("UPDATE stok SET jumlah = jumlah - 1")
        koneksi.execute("COMMIT")
        print("[penulis-2] COMMIT berhasil")
    except sqlite3.OperationalError as galat:
        print("[penulis-2] transaksi gagal:", galat)
    finally:
        koneksi.close()


def jalankan_race(label, timeout):
    siapkan_db()
    t1 = threading.Thread(target=penulis_pertama)
    t2 = threading.Thread(target=penulis_kedua, args=(timeout,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    koneksi = sqlite3.connect(DB)
    print(label, "-> stok akhir:",
          koneksi.execute("SELECT jumlah FROM stok").fetchone()[0])
    koneksi.close()
    print()


jalankan_race("race tanpa busy timeout (timeout=0)", timeout=0)
jalankan_race("race dengan busy timeout (timeout=5)", timeout=5)

Output yang Dihasilkan:

text
[penulis-1] UPDATE tercatat, menahan kunci tulis 1 detik
[penulis-2] transaksi gagal: database is locked
[penulis-1] COMMIT berhasil
race tanpa busy timeout (timeout=0) -> stok akhir: 9

[penulis-1] UPDATE tercatat, menahan kunci tulis 1 detik
[penulis-1] COMMIT berhasil
[penulis-2] COMMIT berhasil
race dengan busy timeout (timeout=5) -> stok akhir: 8

Mari kita telaah baris-baris kode di atas:

  1. Baris 9-16 (siapkan_db): setiap ronde race dimulai dari keadaan bersih — tabel stok dibuat ulang berisi 10 unit, dan koneksi setup ditutup agar tidak menahan kunci.
  2. Baris 19-27 (penulis_pertama): isolation_level=None mematikan transaksi implisit Python sehingga BEGIN dan COMMIT dikendalikan kode; time.sleep(1.0) menahan transaksi tulis terbuka agar tabrakan pasti terjadi.
  3. Baris 30-41 (penulis_kedua): argumen timeout menentukan lama koneksi bersedia menunggu kunci lepas; try/except menangkap sqlite3.OperationalError, kelas galat pembungkus SQLITE_BUSY.
  4. Baris 44-56 (jalankan_race): dua thread berjalan bersamaan, join() menunggu keduanya, lalu stok akhir dibaca dari koneksi segar.

Pada race pertama, penulis kedua menolak menunggu sehingga transaksinya gagal seketika — hanya satu pengurangan tersimpan (stok 9). Pada race kedua, penulis kedua menunggu kunci lepas lalu berhasil — dua pengurangan tersimpan utuh (stok 8). Tabrakan penulis di SQLite diselesaikan dengan antrean atau kegagalan, bukan dengan data campuran.

Snapshot Pembaca dalam Praktik #

Bagaimana dengan pembaca? Skrip berikut menjalankan satu penulis dan satu pembaca bergantian untuk memperlihatkan dua jaminan sekaligus: bebas dirty read dan snapshot yang stabil di dalam transaksi.

python
import sqlite3
from pathlib import Path

DB = Path("toko.db")
if DB.exists():
    DB.unlink()

setup = sqlite3.connect(DB)
setup.execute("CREATE TABLE stok (produk TEXT PRIMARY KEY, jumlah INTEGER)")
setup.execute("INSERT INTO stok VALUES ('Kemeja Batik', 10)")
setup.commit()
setup.close()

penulis = sqlite3.connect(DB, timeout=1, isolation_level=None)
pembaca = sqlite3.connect(DB, isolation_level=None)

penulis.execute("BEGIN")
penulis.execute("UPDATE stok SET jumlah = jumlah - 1")  # 10 -> 9, belum COMMIT

print("pembaca (autocommit) melihat:", pembaca.execute(
    "SELECT jumlah FROM stok").fetchone()[0])

pembaca.execute("BEGIN DEFERRED")
print("pembaca (mulai transaksi) melihat:", pembaca.execute(
    "SELECT jumlah FROM stok").fetchone()[0])

try:
    penulis.execute("COMMIT")
    print("penulis: COMMIT langsung berhasil")
except sqlite3.OperationalError as galat:
    print("penulis: COMMIT ditolak sementara ->", galat)

print("pembaca (transaksi berlanjut) melihat:", pembaca.execute(
    "SELECT jumlah FROM stok").fetchone()[0])

pembaca.execute("COMMIT")  # transaksi baca selesai, kunci baca dilepas
penulis.execute("COMMIT")  # percobaan kedua berhasil

print("pembaca (setelah semuanya) melihat:", pembaca.execute(
    "SELECT jumlah FROM stok").fetchone()[0])
penulis.close()
pembaca.close()

Output yang Dihasilkan:

text
pembaca (autocommit) melihat: 10
pembaca (mulai transaksi) melihat: 10
penulis: COMMIT ditolak sementara -> database is locked
pembaca (transaksi berlanjut) melihat: 10
pembaca (setelah semuanya) melihat: 9

Empat baris pertama output menunjukkan pembaca selalu melihat angka 10 meskipun penulis sudah menulis angka 9 — tidak ada dirty read, dan pembacaan berulang di dalam satu transaksi menghasilkan nilai yang sama. COMMIT penulis justru ditolak sementara karena pembaca masih menahan kunci baca; setelah transaksi pembaca selesai, penulis berhasil meng-commit. Detail model isolasi SQLite tersedia di Isolation In SQLite ; perbandingan keempat level di PostgreSQL ada di Transaction Isolation .

Isolation level menjawab pertanyaan “apa yang boleh terlihat”. Pertanyaan berikutnya: bagaimana database mencapai isolasi itu tanpa memaksa semua transaksi mengantri — lewat kunci pesimistis, kontrol optimistis, atau MVCC? Chapter Concurrency Control membedah tiga mekanisme tersebut sekaligus skenario deadlock yang nyata.