Snapshot isolation dapat membiarkan dua transaksi commit meskipun hasil gabungannya melanggar aturan yang sudah diperiksa masing-masing transaksi sebelum melakukan write. Anomali ini muncul saat kedua transaksi membaca kondisi logis yang sama, lalu menulis row yang berbeda. Karena write set keduanya tidak beririsan, deteksi konflik write-write biasa tidak memiliki benturan untuk ditolak.

Kondisi ini disebut write skew. Batas masalahnya berada di antara invariant aplikasi dan isolation database: sebuah transaksi dapat melihat snapshot yang konsisten, tetapi tetap ikut menghasilkan state akhir yang akan gagal terhadap predicate yang sebelumnya diperiksa.

Snapshot konsisten tidak membuat keputusan menjadi serial

Bayangkan tabel jadwal on-call dengan aturan bahwa setidaknya satu dokter harus tetap tersedia:

CREATE TABLE on_call (
    doctor_id bigint PRIMARY KEY,
    available boolean NOT NULL
);

Anggap row dokter 10 dan 20 sama-sama memiliki available = true. Dua transaksi dimulai dari snapshot yang memuat state tersebut. Keduanya menghitung dokter yang tersedia dan mendapat hasil dua. Transaksi A mengubah dokter 10 menjadi tidak tersedia; transaksi B melakukan hal yang sama pada dokter 20.

Secara konseptual, operasi keduanya berbentuk:

SELECT count(*)
FROM on_call
WHERE available = true;

UPDATE on_call
SET available = false
WHERE doctor_id = ?;

Masing-masing transaksi mengambil keputusan dari state database yang koheren. Tidak ada yang membaca commit parsial. Masalahnya, predicate yang dibaca kedua transaksi mencakup state yang lebih luas daripada row yang ditulis masing-masing transaksi.

Jika implementasi isolation mengizinkan keduanya commit karena A menulis row 10 dan B menulis row 20, state akhir memiliki nol dokter tersedia. Kedua pemeriksaan lokal lolos, tetapi invariant lintas row tidak bertahan terhadap eksekusi konkuren.

Konflik berada pada predicate, bukan satu row bersama

Lost update lebih mudah terlihat karena transaksi yang bersaing biasanya menulis item yang sama. Version column, kondisi compare-and-swap, atau row lock dapat membuat benturan tersebut eksplisit.

Write skew memiliki bentuk berbeda. Kedua transaksi berkonflik melalui makna data. Setiap write mengubah kebenaran predicate yang dipakai transaksi lain sebagai dasar keputusan:

count(dokter tersedia) >= 1

Tidak harus ada satu row yang menyimpan invariant itu. Menganggap konflik write pada level row sebagai seluruh batas concurrency berarti dependency ini terlewat.

Perbedaan tersebut juga memisahkan write skew dari dirty read. Setiap transaksi dapat hanya membaca data committed dari snapshot miliknya. Anomali ini tidak memerlukan satu transaksi melihat state uncommitted milik transaksi lain.

Snapshot isolation dan serializable isolation memberi kontrak berbeda

Snapshot isolation sering diimplementasikan dengan multiversion concurrency control, tetapi MVCC sendiri tidak mendefinisikan satu kontrak isolation universal. Semantik produk dan pengaturan transaksi menentukan history mana yang boleh commit.

Pada snapshot isolation, transaksi umumnya membaca dari snapshot stabil dan update konkuren terhadap item berversi yang sama dicegah agar tidak sama-sama berhasil. Kombinasi itu menutup beberapa anomali umum, tetapi tidak dengan sendirinya menolak setiap dependency cycle yang melibatkan predicate read dan write terpisah.

Serializable isolation memiliki kontrak lebih kuat: efek transaksi yang commit harus ekuivalen dengan suatu urutan eksekusi serial. History saat kedua dokter secara independen keluar dari jadwal tidak dapat memenuhi kontrak tersebut jika masing-masing transaksi mensyaratkan dokter lain tetap tersedia.

Implementasi untuk menyediakan serializability berbeda antar-database. Sistem dapat memakai predicate atau range locking, analisis serialization graph, atau skema concurrency control lain. Aplikasi sebaiknya bergantung pada kontrak isolation yang didokumentasikan, bukan menyimpulkan jaminan hanya dari keberadaan MVCC.

Row lock efektif jika cakupannya sama dengan invariant

Salah satu pendekatan adalah mengunci semua row yang state-nya terlibat dalam keputusan. Untuk himpunan kecil dan stabil, transaksi dapat mengambil row terkait dengan locking read sebelum mengevaluasi aturan. Transaksi konkuren kemudian bersaing pada lock set yang sama.

Pendekatan ini menjadi rapuh ketika predicate menggambarkan himpunan yang berubah. Query seperti WHERE available = true tidak otomatis berarti setiap database melindungi ketiadaan atau kemunculan row yang cocok dengan cara yang sama. Perlindungan range dan predicate bergantung pada database serta level isolation.

Desain yang lebih eksplisit dapat menambahkan row yang mewakili serialization point. Sebagai contoh, row grup on-call dapat dikunci sebelum perubahan keanggotaan. Transaksi yang mengubah row dokter berbeda kemudian sengaja dibuat bersaing pada row grup yang sama.

Cara ini tidak membuat invariant otomatis terjaga. Row tersebut menyediakan objek sinkronisasi bersama, dan correctness tetap bergantung pada semua jalur mutasi yang mengikuti protokol itu.

Constraint paling kuat saat invariant cocok dengan modelnya

Database constraint dapat mengeluarkan timing aplikasi dari batas correctness ketika invariant dapat dinyatakan langsung oleh database. Unique constraint adalah contoh umum: transaksi konkuren dapat berlomba mengklaim logical key yang sama, sementara database menentukan state akhir yang diterima.

Tidak semua aturan lintas row mudah dipetakan ke declarative constraint. Kondisi agregat seperti jumlah minimum sering memerlukan representasi berbeda, serialization eksplisit, atau transaksi serializable. Trigger dapat memusatkan enforcement pada beberapa sistem, tetapi perilaku concurrency-nya tetap bergantung pada semantik locking dan isolation untuk data yang diperiksa.

Pertanyaan desain yang relevan bukan sekadar lokasi kode validasi. Mekanisme yang memvalidasi predicate juga harus mencegah transaksi konkuren membatalkan premis tersebut sebelum commit.

Retry menjadi bagian dari penanganan konflik serializable

Eksekusi serializable tidak berarti setiap transaksi pasti commit pada percobaan pertama. Implementasi yang mendeteksi pola dependency berbahaya dapat membatalkan salah satu peserta agar history yang diterima tetap serializable.

Aplikasi yang memakai isolation tersebut perlu memperlakukan serialization failure sebagai hasil pada level transaksi. Retry harus menjalankan ulang seluruh transaksi dari snapshot baru, termasuk read yang menghasilkan keputusan. Mengulang hanya UPDATE terakhir akan mempertahankan keputusan yang berasal dari state yang sudah ditolak database sebagai tidak aman.

Kebijakan retry juga memerlukan batas yang finite. Contention yang bertahan dapat terus menghasilkan konflik, sehingga caller mungkin perlu menerima kegagalan eksplisit setelah sejumlah percobaan terbatas, bukan loop tanpa batas.

Invariant menentukan batas sinkronisasi

Write skew memperlihatkan ketidakselarasan antara write yang berbentuk row dan keputusan yang cakupannya mengikuti invariant. Sebuah transaksi dapat konsisten secara internal, tidak menyentuh row yang ditulis transaksi lain, tetapi tetap ikut menghasilkan state committed yang tidak valid.

Penanganannya mengikuti cakupan invariant yang sebenarnya. Serializable isolation dapat membuat database menolak history yang tidak serializable. Lock eksplisit dapat memaksa transaksi melewati serialization point bersama. Declarative constraint dapat menyerahkan arbitrasi aturan langsung kepada database saat model data mendukungnya.

Pemeriksaan yang terpisah dari mekanisme concurrency yang melindungi premisnya tidak cukup. Jika correctness bergantung pada beberapa row yang harus tetap memiliki hubungan tertentu, hubungan tersebut—bukan setiap row secara terpisah—merupakan unit yang harus dipertahankan concurrency control.