WAL dalam Praktik

Pembaca dan penulis yang berjalan bersamaan pada mode WAL.

Daftar isi

Keunggulan utama mode WAL adalah pembaca dan penulis yang berjalan bersamaan tanpa saling memblokir, dan sifat itu paling mudah dipahami lewat percobaan dua koneksi. Topik ini membuktikan perilaku konkurensi tersebut dengan skrip Python yang urutannya tetap, membandingkannya dengan mode journal rollback, lalu menjelaskan peran berkas -wal dan -shm serta mekanisme checkpoint otomatis dan manual beserta konsekuensi ukuran berkasnya.

Snapshot untuk pembaca#

Pada mode WAL, pembaca yang membuka transaksi melihat snapshot, yaitu potret keadaan basis data pada saat transaksinya dimulai. Potret itu tidak berubah walaupun penulis melakukan COMMIT di sela-sela pembacaan, karena catatan perubahan penulis menumpuk di berkas -wal tanpa menyentuh berkas yang sedang dibaca. Sifat ini disebut isolasi snapshot dan menjadi dasar kenyamanan aplikasi yang lama membaca.

Skrip berikut menyiapkan satu koneksi penulis dan satu koneksi pembaca pada basis data perpustakaan mode WAL. Pembaca membuka transaksi dan menghitung jumlah buku, penulis menambah satu buku di sela transaksi itu, lalu jumlah buku dibaca lagi dua kali.

python snapshot-wal.py
import os
import sqlite3

DB = "wal-snapshot.db"
if os.path.exists(DB):
    os.remove(DB)

penulis = sqlite3.connect(DB, isolation_level=None)
penulis.execute("PRAGMA journal_mode = WAL")
penulis.execute("BEGIN")
penulis.execute("CREATE TABLE buku (id INTEGER PRIMARY KEY, judul TEXT NOT NULL)")
penulis.execute("INSERT INTO buku (judul) VALUES ('Anak Sungai')")
penulis.execute("INSERT INTO buku (judul) VALUES ('Rumah Pohon')")
penulis.execute("COMMIT")

pembaca = sqlite3.connect(DB, isolation_level=None)
pembaca.execute("BEGIN")
print("pembaca memulai transaksi, jumlah baris:", pembaca.execute("SELECT COUNT(*) FROM buku").fetchone()[0])

penulis.execute("BEGIN")
penulis.execute("INSERT INTO buku (judul) VALUES ('Lentera Malam')")
penulis.execute("COMMIT")
print("penulis melakukan commit tanpa terhalang pembaca")

print("pembaca masih membaca snapshot:", pembaca.execute("SELECT COUNT(*) FROM buku").fetchone()[0])
pembaca.execute("COMMIT")
print("setelah transaksi pembaca selesai:", pembaca.execute("SELECT COUNT(*) FROM buku").fetchone()[0])

penulis.close()
pembaca.close()
text Keluaran
pembaca memulai transaksi, jumlah baris: 2
penulis melakukan commit tanpa terhalang pembaca
pembaca masih membaca snapshot: 2
setelah transaksi pembaca selesai: 3

Empat baris keluaran itu merangkum seluruh perilaku mode WAL. COMMIT penulis selesai meskipun pembaca masih menahan transaksinya, sedangkan pembaca tetap melihat dua baris karena transaksinya mengunci potret lama. Setelah transaksi pembaca selesai, pembacaan berikutnya melihat tiga baris. Tidak ada satu pun pihak yang menunggu pihak lain.

Kontras dengan mode journal rollback#

Perilaku yang sama gagal terjadi pada mode journal rollback, dan perbandingan ini memperlihatkan apa sebenarnya yang dibeli dengan mode WAL. Pada mode rollback, pembaca memegang lock SHARED selama transaksinya terbuka, sedangkan penulis membutuhkan lock EXCLUSIVE untuk menyelesaikan COMMIT. Penulis yang memaksa COMMIT di tengah transaksi pembaca akhirnya menemui error busy.

python kontras-rollback.py
import os
import sqlite3

DB = "kontras-rollback.db"
if os.path.exists(DB):
    os.remove(DB)

penulis = sqlite3.connect(DB, isolation_level=None, timeout=0.05)
penulis.execute("PRAGMA journal_mode = DELETE")
penulis.execute("BEGIN")
penulis.execute("CREATE TABLE buku (id INTEGER PRIMARY KEY, judul TEXT NOT NULL)")
penulis.execute("INSERT INTO buku (judul) VALUES ('Anak Sungai')")
penulis.execute("INSERT INTO buku (judul) VALUES ('Rumah Pohon')")
penulis.execute("COMMIT")

pembaca = sqlite3.connect(DB, isolation_level=None, timeout=0.05)
pembaca.execute("BEGIN")
print("pembaca memulai transaksi, jumlah baris:", pembaca.execute("SELECT COUNT(*) FROM buku").fetchone()[0])

try:
    penulis.execute("BEGIN")
    penulis.execute("INSERT INTO buku (judul) VALUES ('Lentera Malam')")
    penulis.execute("COMMIT")
    print("penulis berhasil commit")
except sqlite3.OperationalError as e:
    print("error penulis:", e)

pembaca.execute("COMMIT")
print("setelah pembaca selesai, penulis melanjutkan transaksinya")
penulis.execute("COMMIT")
print("jumlah baris sekarang:", penulis.execute("SELECT COUNT(*) FROM buku").fetchone()[0])

penulis.close()
pembaca.close()
text Keluaran
pembaca memulai transaksi, jumlah baris: 2
error penulis: database is locked
setelah pembaca selesai, penulis melanjutkan transaksinya
jumlah baris sekarang: 3

COMMIT penulis ditolak dengan pesan database is locked selagi pembaca masih aktif, kemudian transaksi yang sama selesai begitu pembaca melepas locknya. Perhatikan bahwa transaksi penulis tidak hilang saat COMMIT gagal, karena pernyataan yang sudah dijalankan tetap tertahan sampai COMMIT berikutnya berhasil. Nilai timeout yang sengaja dikecilkan menjadi 0.05 detik membuat percobaan ini cepat sekaligus tetap menentukan hasilnya, dan topik lock dan error busy membahas parameter itu lebih dalam.

Berkas -wal dan -shm#

Dua berkas samping mode WAL masing-masing punya tugas yang jelas. Berkas -wal menyimpan deretan frame, yakni catatan halaman yang ditambahkan setiap COMMIT, lengkap dengan nomor urut yang membedakan catatan sah dari catatan lama. Berkas -shm, singkatan dari shared memory, memuat indeks yang memetakan setiap halaman ke frame terbarunya sehingga pembaca tidak perlu memindai seluruh -wal.

Indeks pada -shm hidup di memori bersama yang dipakai bersama seluruh proses pada mesin yang sama, dan inilah alasan mode WAL menuntut satu host. Apabila berkas -shm hilang karena kerusakan atau penghapusan, koneksi berikutnya membangunnya ulang dari isi -wal, sehingga berkas itu bersifat bantu dan bukan gudang data. Selama setidaknya satu koneksi terbuka, kedua berkas tetap ada, dan keduanya dibersihkan ketika koneksi terakhir ditutup dengan rapi.

Ukuran -wal tumbuh mengikuti jumlah perubahan yang belum dipindahkan ke basis data utama. Pembaca yang menahan snapshot lama membuat checkpoint tidak dapat membuang frame di bawah posisi bacanya, sehingga -wal membengkak meskipun penulis rajin mengomit. Aplikasi dengan transaksi besar atau pembaca berumur panjang dengan demikian perlu mengenal checkpoint secara manual.

Checkpoint otomatis dan manual#

Checkpoint adalah proses memindahkan frame-frame sah dari -wal ke berkas basis data utama. SQLite menjalankannya secara otomatis setiap kali -wal mencapai 1000 halaman, dan ambang itu dapat diatur melalui PRAGMA wal_autocheckpoint. Pemicuan manual dilakukan dengan PRAGMA wal_checkpoint yang menerima empat tingkat, yaitu PASSIVE, FULL, RESTART, dan TRUNCATE. Nilai kembaliannya berupa tiga angka, yaitu tanda busy, jumlah halaman di log, dan jumlah halaman yang berhasil dipindahkan.

python checkpoint-wal.py
import os
import sqlite3

DB = "demo-checkpoint.db"
if os.path.exists(DB):
    os.remove(DB)

koneksi = sqlite3.connect(DB, isolation_level=None)
koneksi.execute("PRAGMA journal_mode = WAL")
print("wal_autocheckpoint bawaan:", koneksi.execute("PRAGMA wal_autocheckpoint").fetchone()[0])
koneksi.execute("BEGIN")
koneksi.execute("CREATE TABLE peminjaman (id INTEGER PRIMARY KEY, anggota_id INTEGER NOT NULL, buku_id INTEGER NOT NULL)")
koneksi.executemany("INSERT INTO peminjaman (anggota_id, buku_id) VALUES (?, ?)",
                    ((1 + (i % 30), 1 + (i % 50)) for i in range(500)))
koneksi.execute("COMMIT")
print("checkpoint PASSIVE:", koneksi.execute("PRAGMA wal_checkpoint(PASSIVE)").fetchone())
print("checkpoint TRUNCATE:", koneksi.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone())
print("checkpoint TRUNCATE lagi:", koneksi.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone())
koneksi.close()
text Keluaran
wal_autocheckpoint bawaan: 1000
checkpoint PASSIVE: (0, 4, 4)
checkpoint TRUNCATE: (0, 0, 0)
checkpoint TRUNCATE lagi: (0, 0, 0)

Checkpoint PASSIVE melaporkan empat halaman di log dan keempatnya berhasil dipindahkan, tetapi mode ini tidak mengecilkan berkas -wal karena sifatnya tidak menunggu siapa pun. Checkpoint TRUNCATE berikutnya melaporkan log kosong dan mengembalikan panjang -wal menjadi nol, sedangkan pemicuan ulangnya tetap melaporkan tiga angka nol karena tidak ada lagi yang tersisa. Perbedaan yang berhasil dipindahkan dengan berkas yang mengecil itulah sebabnya ada dua baris berbeda untuk pekerjaan yang tampak serupa.

Ukuran berkas menjadi pertukaran yang nyata pada mode WAL. Basis data tampak berupa tiga berkas alih-alih satu, dan ruang disk dapat dipakai ganda selama -wal belum di-checkpoint. Di sisi lain, seluruh pembacaan tetap secepat biasa dan tidak ada halaman yang hilang, karena checkpoint yang benar tidak pernah menghapus frame yang masih dibutuhkan snapshot mana pun.

Ringkasan#

  • Pembaca pada mode WAL membuka snapshot, sehingga COMMIT penulis tidak tertahan dan pembacaan tetap konsisten.
  • Pada mode journal rollback, transaksi pembaca yang terbuka menolak COMMIT penulis dengan error busy.
  • Berkas -wal menyimpan frame perubahan dan -shm memuat indeks berbagi memori untuk menemukan frame terbaru.
  • Checkpoint memindahkan frame ke basis data utama, berjalan otomatis pada 1000 halaman atau manual melalui PRAGMA wal_checkpoint, dan tingkat TRUNCATE mengecilkan -wal menjadi nol.

Referensi#