Write Skew Dapat Merusak Invariant Lintas Baris pada Snapshot Isolation
Snapshot isolation memberi setiap transaksi view database yang stabil dan umumnya mencegah transaksi concurrent melakukan commit atas write yang saling bertabrakan pada baris yang sama. Properti concurrency ini kuat, tetapi tidak membuat setiap invariant aplikasi menjadi serializable.
Write skew muncul ketika dua transaksi membaca state yang saling beririsan, mengambil keputusan dari snapshot valid yang sama, lalu menulis record yang berbeda. Karena write set keduanya tidak bertabrakan, kedua commit dapat berhasil. State gabungannya dapat melanggar aturan yang sudah diperiksa masing-masing transaksi sebelum menulis.
Snapshot valid dapat menghasilkan state yang tidak valid
Bayangkan roster on-call dengan dua operator. Aturannya sederhana: setidaknya satu operator harus tetap on call.
state awal
operator A = on
operator B = onDua transaksi dimulai dari state tersebut. Transaksi T1 hendak menonaktifkan A dari on-call. T2 hendak menonaktifkan B. Masing-masing transaksi memeriksa bahwa operator lain masih tersedia.
snapshot T1: A=on, B=on
snapshot T2: A=on, B=on
T1 melihat B=on -> menulis A=off
T2 melihat A=on -> menulis B=offWrite menyasar baris yang berbeda. Implementasi snapshot isolation yang mendeteksi write-write conflict karena itu dapat mengizinkan kedua transaksi melakukan commit.
state akhir
operator A = off
operator B = offTidak ada transaksi yang melihat snapshot tidak valid. State tidak valid muncul dari gabungan kedua commit.
Deteksi konflik baris lebih sempit daripada invariant
Aturan uniqueness yang didukung database constraint memiliki titik enforcement yang konkret. Predicate lintas baris seperti “setidaknya satu operator tetap on call” mungkin tidak terpetakan ke satu baris yang selalu diubah oleh setiap transaksi.
Aplikasi dapat menjalankan query sebelum update:
SELECT count(*)
FROM operators
WHERE on_call = true;Hasil 2 tidak membuat fakta tersebut menjadi reserved selama sisa transaksi. Transaksi concurrent lain dapat mengambil keputusan yang kompatibel dari snapshot miliknya.
Race ini bukan akibat check aplikasi yang hilang. Kedua transaksi dapat menjalankan check dengan benar. Persoalannya, isolation level tidak selalu mengurutkan predicate read terhadap write terpisah milik transaksi lain.
Shared write dapat menciptakan konflik
Salah satu mitigasi adalah membuat transaksi yang menjaga invariant yang sama berkontensi pada record bersama. Sebagai contoh, baris on-call group dapat menjadi serialization point.
group row: primary-on-call
T1 lock group row -> periksa roster -> update A -> commit
T2 menunggu -> periksa roster baru -> ambil keputusanShared row mengubah konflik logis menjadi konflik fisik yang dapat dikoordinasikan database. Pendekatan ini sering praktis ketika invariant memiliki scope yang jelas, misalnya satu team, account, inventory pool, atau scheduling bucket.
Record untuk serialization sebaiknya mengikuti scope tersebut. Satu global lock row dapat menjaga correctness, tetapi menimbulkan contention yang tidak perlu di antara group independen.
Serializable isolation dapat menolak eksekusi berbahaya
Serializable isolation bertujuan membuat transaksi yang berhasil commit setara dengan suatu urutan serial. Implementasi database mencapai properti itu melalui mekanisme berbeda, termasuk locking dan deteksi pola dependency.
Untuk kasus roster, database serializable dapat memblokir salah satu transaksi atau membatalkannya dengan serialization failure. Aplikasi kemudian perlu menangani failure sesuai kontrak database, sering kali dengan retry seluruh transaksi dari state baru.
Retry bukan sekadar mengulang UPDATE terakhir. Keputusan bergantung pada read sebelumnya, sehingga seluruh unit keputusan membutuhkan transaksi baru dan view baru.
begin
baca state invariant
ambil keputusan
write
commitJika commit melaporkan serialization failure, seluruh unit tersebut menjadi boundary retry.
Explicit lock memerlukan target yang tepat
SELECT ... FOR UPDATE berguna ketika baris yang menentukan keputusan sudah diketahui dan dapat dikunci. Mekanisme ini bukan predicate lock universal.
Misalnya, sebuah aturan bergantung pada tidak adanya baris yang cocok. Dalam kondisi itu mungkin tidak ada baris existing yang dapat dikunci. Range locking, predicate locking, advisory locking, atau record khusus untuk serialization mungkin diperlukan, bergantung pada database dan access pattern.
Bahkan ketika baris sudah ada, setiap code path yang dapat mengubah state terlindungi harus mengikuti coordination protocol yang sama. Lock pada satu endpoint tidak menjaga invariant jika writer lain melewatinya.
Constraint lebih baik ketika aturan dapat diekspresikan
Database constraint menempatkan validasi pada boundary perubahan state. Unique constraint, foreign key, exclusion constraint, dan CHECK constraint yang sesuai dapat menghilangkan kelas race di aplikasi ketika aturan dapat diekspresikan secara langsung.
Tidak semua invariant lintas baris cocok dengan declarative constraint. Jika cocok, constraint umumnya lebih mudah diaudit daripada convention yang tersebar di banyak call site aplikasi.
Jika tidak cocok, desain membutuhkan mekanisme concurrency eksplisit dengan scope yang sesuai dengan invariant.
Testing membutuhkan transaksi yang overlap
Test sequential tidak dapat mengekspos write skew. Kedua operasi perlu memegang snapshot yang overlap dalam waktu.
Concurrency test yang berguna mengoordinasikan dua koneksi database:
T1 begin
T2 begin
T1 read predicate
T2 read predicate
T1 write row A
T2 write row B
T1 commit
T2 commitHasil yang diharapkan bergantung pada proteksi yang dipilih. Pada eksekusi rentan, kedua commit berhasil dan invariant gagal. Dengan serialization point atau perilaku serializable yang sesuai, salah satu transaksi menunggu, melihat state yang sudah berubah, atau gagal saat commit.
Test sebaiknya berjalan pada database engine dan konfigurasi isolation yang benar-benar digunakan di production. Semantic transaksi berbeda antar-engine, sedangkan test double yang menjalankan transaksi secara sequential tidak dapat membuktikan perilaku concurrency yang relevan.
Isolation level adalah bagian dari correctness aplikasi
Kode transaksi sering tampak benar secara lokal karena setiap read, condition, dan update benar ketika dilihat sendiri. Concurrency menambahkan kemungkinan eksekusi yang tidak terlihat dalam satu request trace.
Untuk invariant yang mencakup banyak record, correctness bergantung pada hubungan read dan write lintas transaksi. Snapshot isolation dapat menyediakan view yang konsisten sambil tetap mengizinkan write skew. Solusinya adalah memberi invariant titik enforcement: database constraint, shared serialization record, explicit locking dengan scope yang sesuai, atau serializable isolation dengan penanganan retry yang benar.