Write Skew Dapat Merusak Invarian pada Snapshot Isolation

Snapshot isolation memberi setiap transaction pandangan stabil terhadap data yang sudah commit dan biasanya menolak update konkuren pada row yang sama. Kombinasi ini menghilangkan banyak anomali yang muncul pada isolation level yang lebih lemah. Namun, tidak setiap invarian aplikasi otomatis menjadi serializable.

Write skew adalah kasus penting. Dua transaction membaca state yang saling beririsan, mengambil keputusan dari snapshot valid yang sama, lalu mengubah row yang berbeda. Karena write set keduanya tidak bertabrakan, keduanya dapat commit. Hasil gabungannya dapat melanggar aturan yang tetap terpenuhi pada snapshot masing-masing transaction.

Aturan lintas row membuka celah

Bayangkan tabel jadwal jaga dengan dua dokter. Aturan operasional mewajibkan sedikitnya satu dokter tetap bertugas:

doctor | on_call
-------+--------
A      | true
B      | true

Transaction T1 memeriksa kedua row, melihat B masih bertugas, lalu mengubah A menjadi false. Pada saat yang sama, T2 memeriksa snapshot yang sama, melihat A masih bertugas, lalu mengubah B menjadi false.

Alurnya:

T1: read A=true, B=true
T2: read A=true, B=true

T1: write A=false
T2: write B=false

T1: commit
T2: commit

Tidak ada row yang menerima dua write konkuren. Pemeriksaan first-committer-wins pada row individual tidak memiliki konflik langsung untuk ditolak. Setelah kedua commit selesai, kedua nilai menjadi false dan aturan lintas row gagal.

Masing-masing transaction mengambil keputusan yang valid secara lokal. Anomali baru terlihat ketika efek keduanya digabungkan.

Snapshot isolation melindungi versi, bukan sembarang predicate

Implementasi snapshot isolation umumnya memberi transaction snapshot database yang konsisten dari satu titik waktu logis. Read tetap melihat versi yang sesuai dengan snapshot tersebut walaupun transaction lain melakukan commit versi yang lebih baru saat transaction pertama masih berjalan.

Untuk write, database mendeteksi update konkuren yang bertabrakan sesuai aturan concurrency control. Dua transaction yang mencoba mengganti row yang sama biasanya tidak dapat commit secara independen.

Contoh jadwal jaga menghindari tabrakan tersebut. T1 menulis row A dan T2 menulis row B. Aturan bisnis mencakup keduanya, sedangkan konflik storage dievaluasi pada target write yang terpisah.

Pembedaan ini penting karena invarian seperti “sedikitnya satu row yang cocok harus tetap aktif” merupakan predicate atas sekumpulan data. Aturan itu tidak otomatis direpresentasikan sebagai konflik pada satu record fisik.

Anomali ini berbeda dari lost update

Lost update terjadi ketika pekerjaan konkuren menargetkan nilai logis yang sama dan satu update menimpa atau mengaburkan update lain. Write skew dapat terjadi meskipun setiap row yang berubah hanya memiliki satu writer.

Perbedaan tersebut memengaruhi solusi. Menambahkan version column pada setiap row dokter dapat mendeteksi dua writer yang berebut dokter yang sama, tetapi tidak membuat T1 dan T2 bertabrakan saat keduanya memang mengubah dokter yang berbeda.

Masalahnya bukan sekadar apakah setiap row diperbarui secara aman. Transaction boundary juga harus melindungi hubungan antar-row yang membentuk invarian.

Eksekusi serial akan menolak salah satu keputusan

Jalankan operasi yang sama secara serial. Jika T1 commit lebih dahulu, T2 kemudian membaca A sebagai false. Dengan penerapan aturan yang benar, T2 harus mempertahankan B sebagai dokter yang bertugas. Urutan sebaliknya menghasilkan kondisi simetris.

Tidak ada urutan serial yang memungkinkan kedua transaction membaca state awal dengan dua nilai true, lalu keduanya mematikan row masing-masing sambil tetap mempertahankan aturan.

Itulah sifat diagnostik utamanya. State akhir tersebut dapat muncul pada snapshot isolation, tetapi tidak pada eksekusi serial yang menerapkan logika keputusan yang sama. Serializable isolation dirancang untuk mencegah hasil semacam ini, walaupun mekanismenya berbeda antar-database.

Serializable isolation dapat melakukan abort tanpa blocking luas

Eksekusi serializable tidak mengharuskan setiap database memasang lock luas pada seluruh read. Sebagian sistem memakai predicate atau range locking. Sistem lain melacak dependency read-write lalu melakukan abort pada transaction ketika dependency graph menunjukkan anomali serialisasi.

Aplikasi karena itu perlu memperlakukan serialization failure sebagai hasil concurrency yang normal. Transaction yang logikanya benar tetap dapat diminta mengulang karena snapshot yang diamatinya tidak lagi dapat ditempatkan secara aman dalam riwayat serial.

Retry juga harus memiliki batas yang tepat. Seluruh transaction pengambilan keputusan perlu dijalankan kembali terhadap pandangan baru; mengulang hanya UPDATE terakhir akan mempertahankan keputusan lama yang memicu konflik.

Explicit locking dapat membuat konflik menjadi konkret

Ketika invarian memiliki titik koordinasi yang kecil dan stabil, explicit locking dapat mengubah konflik logis menjadi konflik fisik.

Sebagai contoh, aplikasi dapat mengunci parent record yang mewakili grup jadwal sebelum memeriksa dan mengubah row anggota:

BEGIN;
SELECT id FROM on_call_group
WHERE id = 42
FOR UPDATE;

SELECT doctor, on_call
FROM on_call_member
WHERE group_id = 42;

-- validasi invarian lalu update satu anggota
COMMIT;

Transaction konkuren untuk grup yang sama kemudian berebut parent row. Lock tersebut membuat keputusan untuk grup itu berjalan secara serial walaupun update akhirnya menargetkan row anggota yang berbeda.

Pendekatan ini sederhana ketika tersedia record koordinasi yang alami. Throughput dapat turun jika pekerjaan yang sebenarnya tidak berkaitan dipaksa melewati lock yang terlalu luas.

Constraint membantu jika invarian dapat direpresentasikan

Database constraint bernilai tinggi karena pemeriksaan correctness berada dekat dengan data, tetapi tidak semua predicate lintas row cocok dengan CHECK constraint sederhana. Pemeriksaan pada satu row umumnya tidak dapat menyatakan properti aggregate atas peer row yang bebas berubah.

Dalam beberapa desain, schema dapat diubah agar invarian menjadi unique key, foreign key, exclusion rule, atau update pada satu aggregate row yang dijaga. Database kemudian memperoleh objek konkret yang dapat menjadi titik konflik bagi transaction konkuren.

Perubahan schema tetap harus mempertahankan aturan bisnis sebenarnya. Counter sintetis, misalnya, membawa kewajiban pemeliharaan tersendiri. Jika counter dapat berbeda dari row anggota, masalah correctness hanya berpindah tempat.

Test perlu jadwal konkuren, bukan hanya kasus berurutan

Unit test berurutan dapat memverifikasi aturan keputusan tetapi sama sekali tidak menangkap write skew. Concurrency test yang berguna menahan dua transaction tetap terbuka, membiarkan keduanya membaca state awal yang valid, lalu mengizinkan write dan commit berjalan dalam urutan yang dikendalikan.

Assertion ditempatkan pada invarian setelah kedua transaction selesai. Bergantung pada mekanisme perlindungan yang dipilih, hasil yang diharapkan dapat berupa satu serialization failure, satu lock wait yang diikuti keputusan berbeda, atau konflik lain yang spesifik pada database.

Perilaku retry juga perlu diuji. Jika serialization failure diulang, retry harus mengulangi setiap read yang berkontribusi pada keputusan.

Isolation level adalah bagian dari data model

Pemilihan isolation level bukan hanya pengaturan performa. Pilihan tersebut menentukan riwayat concurrency yang harus dapat ditoleransi aplikasi.

Snapshot isolation cukup kuat untuk banyak workload dan dapat memberi perilaku read yang baik. Pemeriksaan write conflict tetap bekerja pada write konkret, sehingga aturan yang mencakup beberapa record yang dapat diubah secara independen memerlukan analisis tersendiri.

Untuk setiap invarian penting, identifikasi row atau predicate yang membentuknya, lalu identifikasi transaction konkuren yang dapat mengubah fakta tersebut. Jika dua snapshot valid dapat menghasilkan write terpisah yang ketika digabung merusak aturan, desain memerlukan mekanisme serialisasi yang lebih kuat atau titik koordinasi eksplisit.

Boundary yang aman membuat invarian bisnis dan konflik concurrency merujuk pada state yang sama.