Kolom Versi Mengubah Lost Update Menjadi Konflik yang Terdeteksi
Alur read-modify-write dapat menimpa perubahan lain yang sudah commit walaupun setiap statement database berhasil. Dua client membaca row yang sama, menghitung replacement berbeda, lalu menulis secara berurutan. Tanpa kondisi yang mengikat setiap write ke state yang dibacanya, write terakhir dapat menghapus perubahan sebelumnya tanpa sinyal konflik.
Kolom versi membuat dependency tersebut eksplisit. Client membaca data beserta versinya, lalu melakukan update hanya jika versi di storage masih sama dengan yang diamati. Perubahan versi mengubah race menjadi conditional update yang gagal, bukan lost update.
Unconditional update melupakan state yang dibaca
Misalkan sebuah setting account dimulai pada versi 7. Client A dan B sama-sama membacanya sebelum salah satu melakukan write.
A membaca value=X, version=7
B membaca value=X, version=7
A menghitung value=Y
B menghitung value=ZJika keduanya menjalankan unconditional update, B dapat commit setelah A dan mengganti Y dengan Z. Database tidak memiliki predicate yang menunjukkan bahwa hasil perhitungan B bergantung pada versi 7.
Masalahnya bukan concurrent read. Masalahnya adalah write yang tidak memverifikasi apakah input state-nya masih current.
Versi menjadi bagian dari write predicate
Optimistic update biasanya menyertakan versi yang diamati pada klausa WHERE dan menaikkannya dalam statement yang sama:
UPDATE settings
SET value = :new_value,
version = version + 1
WHERE id = :id
AND version = :expected_version;A menulis dengan expected_version = 7, mengubah satu row, lalu memindahkan row ke versi 8. B kemudian menulis dengan expected version yang sama dan tidak mengubah row apa pun.
A: version 7 -> 8 diterima
B: expects 7 conflictAffected-row count merupakan bagian dari protokol. Menganggap nol row sebagai success membuang conflict signal yang sengaja dibuat oleh predicate tersebut.
Comparison dan mutation harus menjadi satu operasi atomic
Membaca current version melalui satu query lalu menjalankan unconditional update pada query berikutnya tidak memberi jaminan yang sama. Transaction lain dapat commit di antara kedua statement.
Database harus mengevaluasi expected version sebagai bagian dari mutation yang mengubah row. Mekanisme setara mencakup compare-and-swap, conditional write, atau fitur optimistic concurrency pada ORM jika fitur tersebut menghasilkan predicate yang diperlukan.
Properti pentingnya adalah atomicity pada storage boundary, bukan bentuk API yang dipakai.
Conflict detection tidak menentukan merge policy
Update yang ditolak menyatakan bahwa state berubah setelah client membacanya. Hasil itu tidak menentukan apakah request baru harus dibuang, diulang, digabungkan, atau ditampilkan kepada pengguna.
Retry yang sekadar membaca ulang lalu menerapkan replacement tetap dapat menghancurkan perubahan yang bermakna. Perilaku retry yang aman bergantung pada operasinya. Increment counter, replacement profile document, dan perubahan satu field pada structured record memiliki merge semantics yang berbeda.
Application code sebaiknya memperlakukan optimistic conflict sebagai outcome tersendiri, bukan menyembunyikannya di balik unconditional retry loop.
Versi harus berubah pada setiap mutation yang dilindungi
Version predicate hanya selengkap mutation path yang memeliharanya. Jika satu code path mengubah protected column tanpa menaikkan versi, client lain dapat memegang snapshot lama dengan versi yang masih terlihat current.
Database trigger, administrative script, background job, bulk update, dan service lain membutuhkan concurrency contract yang sama ketika mengubah protected state.
Timestamp dapat menjadi versi hanya jika precision, update rule, dan comparison semantics-nya membuat collision mustahil untuk workload yang dituju. Integer yang naik secara monotonik sering lebih mudah ditelaah karena setiap mutation yang diterima menghasilkan nilai berikutnya yang berbeda.
Scope versi mengikuti scope konflik
Satu versi untuk seluruh row membuat edit pada field independen tetap berkonflik walaupun sebenarnya dapat hidup berdampingan. Perilaku konservatif ini sering dapat diterima dan menjaga aturan tetap sederhana.
Document yang lebih besar dapat memakai versi yang lebih granular ketika bagian yang tidak berkaitan dapat diubah secara independen. Scope yang lebih halus mengurangi false conflict, tetapi menambah metadata dan membuat invariant lintas field lebih sulit dilindungi.
Version boundary sebaiknya sesuai dengan state yang harus diamati dan diganti secara konsisten.
Delete membutuhkan conditional contract yang sama
Delete dapat race dengan modification seperti update. Delete yang didasarkan pada read sebelumnya sebaiknya menyertakan versi yang diamati jika intervening edit perlu dipertahankan:
DELETE FROM settings
WHERE id = :id
AND version = :expected_version;Nol affected row kemudian berarti record sudah hilang atau berubah sebelum delete commit. Aplikasi dapat membedakan kedua kondisi itu dengan state tambahan jika perilakunya memerlukan perbedaan tersebut.
Optimistic concurrency menukar blocking dengan conflict eksplisit
Kolom versi tidak mencegah dua client bekerja pada waktu yang sama. Keduanya dapat berjalan, sementara storage layer hanya menerima mutation yang didasarkan pada current version.
Pola ini cocok ketika conflict relatif jarang dan menahan database lock selama user interaction atau remote work terlalu mahal. Pada contention tinggi, conflict berulang dapat membuang banyak pekerjaan sehingga serialized execution atau data model lain dapat lebih sesuai.
Kontrak intinya tetap ringkas: baca versi bersama data, sertakan versi tersebut pada mutation predicate, naikkan secara atomic ketika berhasil, dan tangani nol affected row sebagai concurrency outcome. Dengan kontrak itu, overwrite race terlihat sebelum perubahan yang sudah commit terhapus secara diam-diam.