Client database dapat membaca sebuah row, memakai waktu untuk menghitung perubahan, lalu menjalankan UPDATE setelah transaksi lain lebih dulu mengubah row yang sama. Jika statement terakhir hanya mengidentifikasi row lewat primary key, write yang datang kemudian dapat mengganti state yang dihitung dari kondisi lama tanpa konflik yang terlihat.

Kolom versi mengubah batas tersebut. Client membaca state aplikasi beserta nilai revisinya, lalu menyertakan revisi itu dalam predicate update. Database menerima write hanya selama revisi yang tersimpan masih sama dengan state yang sebelumnya diamati client.

Predicate membawa hasil observasi ke operasi write

Anggap sebuah row preferensi akun memiliki kolom version yang selalu meningkat:

SELECT theme, locale, version
FROM user_preferences
WHERE user_id = 42;

Misalnya query menghasilkan version 7. Mutasi berikutnya dapat memasukkan observasi tersebut sebagai syarat:

UPDATE user_preferences
SET theme = 'dark',
    version = version + 1
WHERE user_id = 42
  AND version = 7;

Jika tidak ada transaksi pesaing yang mengubah row, statement cocok dan memajukan version menjadi 8. Jika writer lain sudah commit dan memajukan version, predicate tidak lagi cocok. Statement yang stale mengubah nol row, bukan menimpa state yang lebih baru.

Mekanisme ini bukan lock yang ditahan sejak read awal sampai write terakhir. Mekanismenya berupa transisi state bersyarat yang dievaluasi saat database menjalankan UPDATE.

Jumlah row terdampak merupakan bagian dari kontrak concurrency

Aplikasi harus memeriksa hasil statement bersyarat. Hasil nol row tidak setara dengan persist yang berhasil. Hasil tersebut berarti state yang diharapkan sudah tidak ada ketika predicate write dievaluasi.

Perbedaan ini perlu tetap terlihat melewati abstraction layer. ORM yang menyediakan operasi save() tetap harus menerjemahkan ketidakcocokan versi menjadi hasil concurrency yang dapat dideteksi. Menganggap nol row terdampak sebagai keberhasilan biasa membuang sinyal yang membuat pemeriksaan versi berguna.

Aplikasi kemudian dapat memilih kebijakan yang sesuai dengan mutasi. Aplikasi dapat mengembalikan conflict kepada caller, memuat ulang state terkini lalu menghitung ulang perubahan, atau membatalkan operasi. Retry otomatis hanya tepat jika operasi dapat dihitung ulang terhadap state baru tanpa mengubah semantik yang dimaksud.

Versi harus berubah pada setiap mutasi yang dilindungi

Perbandingan hanya bekerja jika setiap write yang masuk dalam kontrak concurrency mengubah token. Jika satu jalur kode memodifikasi kolom yang dilindungi tanpa memajukan version, client yang memegang token lama tidak dapat mendeteksi mutasi tersebut.

Kondisi ini menciptakan persyaratan ownership terhadap row. SQL dari kode aplikasi, background job, tool administrasi, trigger, dan writer lain harus sepakat mengenai mutasi mana yang mengikuti versioning.

Increment tidak perlu merepresentasikan waktu. Fungsinya adalah membedakan state terlindungi yang berurutan. Counter integer praktis karena transisinya dapat terjadi dalam statement yang sama dengan update data. Token lain dapat menjalankan fungsi serupa jika database dan aplikasi mempertahankan semantik perbandingan atomik yang setara.

Read isolation dan pemeriksaan versi menangani batas yang berbeda

Version predicate tidak menggantikan transaction isolation. Isolation menentukan visibility dan interaksi antaroperasi database. Version predicate menetapkan precondition pada level aplikasi untuk mutasi tertentu.

Dalam alur read-then-write yang umum, read awal dapat terjadi pada satu transaksi atau request, sedangkan update berlangsung jauh kemudian pada transaksi lain. Tidak ada lock database yang melintasi interval tersebut. Nilai version menjadi observasi durable yang dibawa melewati jeda itu.

Di dalam transaksi yang lebih besar, aturan isolation tetap berlaku pada setiap statement. Database juga dapat mendeteksi conflict melalui isolation level tertentu. Aplikasi perlu menangani error database tersebut secara terpisah dari ketidakcocokan versi dengan hasil nol row; keduanya berasal dari mekanisme berbeda meskipun sama-sama dapat mencegah state stale untuk commit.

Kolom versi melindungi row yang direpresentasikan predicate

Satu versi row secara alami terbatas pada state yang tersimpan di row tersebut. Mekanisme ini tidak otomatis melindungi invariant yang melibatkan beberapa row.

Anggap sebuah keputusan bergantung pada dua record, tetapi statement terakhir hanya memeriksa version salah satunya. Transaksi konkuren dapat mengubah record lain tanpa membuat predicate tersebut tidak valid. Conditional update masih dapat berhasil walaupun asumsi multi-row yang dipakai aplikasi sudah stale.

Invariant lintas row memerlukan boundary yang mencakup seluruh state relevan: transaction isolation yang lebih kuat, explicit locking, shared aggregate version, constraint yang dapat dinyatakan oleh database, atau mekanisme lain yang sesuai dengan invariant. Menambahkan field version pada satu row tidak memperluas scope perbandingannya.

Blind increment dapat menghindari siklus read-modify-write

Tidak setiap update memerlukan optimistic version checking. Sebagian mutasi dapat dinyatakan secara atomik dari state database saat ini:

UPDATE counters
SET value = value + 1
WHERE id = 9;

Statement ini tidak menghitung replacement value dari snapshot lama di sisi client. Database mengevaluasi increment terhadap row yang sedang di-update, sehingga pola stale read yang diikuti absolute overwrite tidak terdapat pada operasi ini.

Pemeriksaan versi menjadi relevan ketika penerimaan mutasi bergantung pada caller yang telah mengamati state sebelumnya secara spesifik. Memasang version token secara mekanis pada setiap update dapat menambah penanganan conflict tanpa melindungi keputusan yang benar-benar bergantung pada snapshot.

Delete memerlukan precondition yang sama saat stale delete bermasalah

Client yang stale juga dapat menghapus row setelah writer lain mengubahnya. Jika perubahan di tengah itu seharusnya membatalkan keputusan delete, version perlu masuk ke predicate delete:

DELETE FROM user_preferences
WHERE user_id = 42
  AND version = 7;

Sekali lagi, nol row terdampak menjadi sinyal concurrency. Semantik update dan delete pun tetap selaras: kedua operasi hanya diterima terhadap revisi state yang diklaim sudah diamati caller.

Skema soft delete memerlukan analisis yang sama. Jika deletion direpresentasikan oleh UPDATE, transisi version-nya harus mengikuti aturan mutasi terlindungi yang sama dengan perubahan state lain.

Token menutup race yang sempit tetapi berguna

Kolom versi mengubah stale overwrite dari penggantian diam-diam menjadi failed precondition yang eksplisit pada boundary database. Kekuatan mekanisme ini berasal dari beberapa syarat yang jelas: token dibaca bersama state, diperiksa secara atomik dalam predicate mutasi, dimajukan oleh setiap write yang dilindungi, dan hasil jumlah row diperlakukan sebagai informasi penting.

Boundary ini memang sempit. Mekanisme tersebut tidak melakukan serialisasi workflow arbitrer, tidak melindungi row yang tidak terkait, dan tidak membuat retry otomatis aman secara semantik. Klaim yang diberikan pada satu mutasi sangat spesifik: terapkan perubahan ini hanya jika row masih berada pada revisi yang menjadi dasar perubahan.