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 kemudianROLLBACK, 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 level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
READ UNCOMMITTED | mungkin terjadi | mungkin terjadi | mungkin terjadi |
READ COMMITTED | dicegah | mungkin terjadi | mungkin terjadi |
REPEATABLE READ | dicegah | dicegah | mungkin terjadi |
SERIALIZABLE | dicegah | dicegah | dicegah |
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.
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:
[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: 8Mari kita telaah baris-baris kode di atas:
- Baris 9-16 (
siapkan_db): setiap ronde race dimulai dari keadaan bersih — tabelstokdibuat ulang berisi 10 unit, dan koneksi setup ditutup agar tidak menahan kunci. - Baris 19-27 (
penulis_pertama):isolation_level=Nonemematikan transaksi implisit Python sehinggaBEGINdanCOMMITdikendalikan kode;time.sleep(1.0)menahan transaksi tulis terbuka agar tabrakan pasti terjadi. - Baris 30-41 (
penulis_kedua): argumentimeoutmenentukan lama koneksi bersedia menunggu kunci lepas;try/exceptmenangkapsqlite3.OperationalError, kelas galat pembungkusSQLITE_BUSY. - 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.
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:
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: 9Empat 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.