Optimistic Concurrency Control Menolak Write dari State yang Sudah Usang

Dua client dapat membaca record yang sama, membuat perubahan berbeda, lalu menyimpan dengan selisih beberapa detik. Jika setiap update langsung mengganti value yang tersimpan, write terakhir dapat menghapus perubahan sebelumnya meski kedua request sama-sama dinyatakan berhasil.

Optimistic concurrency control mencegah overwrite diam-diam dengan memberi syarat pada write. Client menyimpan version saat membaca data. Update hanya berhasil jika version tersebut masih berlaku. Version yang sudah berubah membuat write berakhir sebagai conflict, bukan kehilangan perubahan tanpa tanda.

Race dimulai dari read yang valid

Bayangkan sebuah record akun berada pada version 17. Client A dan client B membacanya sebelum salah satu mengirim update.

database: balance_limit=5000, version=17

client A membaca version 17
client B membaca version 17

client A mengatur balance_limit=6000
client B mengatur balance_limit=7000

Tanpa syarat concurrency, kedua write dapat menjadi operasi SQL yang valid. Jika B melakukan commit terakhir, database berakhir pada 7000 dan perubahan A yang sebelumnya diterima hilang.

Masalahnya tidak terbatas pada eksekusi yang benar-benar bersamaan. Kedua request dapat terpisah beberapa detik atau menit. Fakta pentingnya adalah keputusan B dibuat dari state yang sudah usang sebelum B melakukan write.

Version mengubah freshness menjadi predicate untuk write

Implementasi umum menyimpan integer version bersama field yang dapat berubah.

UPDATE accounts
SET balance_limit = 6000,
    version = version + 1
WHERE id = 42
  AND version = 17;

Jika row masih berada pada version 17, tepat satu row berubah dan version menjadi 18. Update berikutnya yang masih membawa version 17 tidak akan cocok dengan row mana pun.

A: WHERE version = 17 -> 1 row berubah -> version 18
B: WHERE version = 17 -> 0 row berubah -> conflict

Hasil nol row memiliki makna pada level aplikasi, bukan sekadar keanehan database. Service perlu membedakan record yang tidak ada dari version conflict jika kontrak API memperlakukan keduanya secara berbeda.

Sebagian database dan ORM menyediakan pola ini melalui compare-and-swap, row version, revision field, atau fitur optimistic locking. Istilahnya dapat berbeda, tetapi invariant-nya sama: write bergantung pada state yang diamati sebelumnya.

Check dan mutation harus atomic

Membaca version saat ini dalam satu statement lalu menjalankan update tanpa syarat pada statement berikutnya masih menyisakan race.

SELECT version ... -> 17
writer lain commit version 18
UPDATE ...          -> menimpa state yang lebih baru

Expected version harus berada di mutation predicate, atau datastore harus menyediakan primitive conditional-write atomic yang setara. Validasi dan mutation perlu menjadi satu keputusan yang tidak dapat disela dari sudut pandang writer yang saling bersaing.

Transaction juga dapat memberi proteksi yang diperlukan jika isolation dan locking semantics-nya sesuai. Namun, menaruh statement read dan write terpisah di dalam transaction tidak otomatis menghasilkan perilaku optimistic concurrency.

Timestamp biasanya merupakan version token yang lebih lemah

Modification timestamp hanya layak menjadi token jika precision, aturan pembentukan, dan comparison semantics-nya membuat collision mustahil untuk workload yang ditangani. Asumsi tersebut mudah gagal.

Integer revision menghindari persoalan sinkronisasi clock dan precision timestamp. Opaque entity tag atau revision value yang dibuat datastore juga dapat dipakai selama setiap mutation yang relevan selalu mengubah token.

Token tidak perlu menunjukkan urutan kepada client. Token hanya perlu berubah setiap kali state yang dilindungi kontrak concurrency berubah.

Conflict adalah hasil normal, bukan perintah retry

Version conflict menandakan precondition untuk write yang diajukan sudah tidak berlaku. Melakukan retry terhadap replacement yang sama memakai version baru secara otomatis dapat menciptakan kembali lost-update bug di layer aplikasi.

Untuk dokumen yang diedit user, service dapat mengembalikan conflict agar client mengambil state terbaru, membandingkan perubahan, lalu mengirim keputusan baru. HTTP API dapat menyatakan conditional mutation dengan entity tag dan If-Match, lalu mengembalikan 412 Precondition Failed ketika validator yang dikirim sudah usang.

Untuk operasi yang dibuat mesin, retry dapat aman jika operasi bisa dihitung ulang dari state terbaru. Increment, misalnya, dapat membaca value baru dan membentuk candidate baru. Replacement yang berasal dari keputusan manusia biasanya memerlukan penanganan lebih hati-hati.

Scope version harus sesuai dengan consistency boundary

Satu version untuk seluruh row berarti field yang tidak saling terkait tetap dapat conflict. Client A mungkin mengubah display name sementara client B mengganti notification setting; shared row version dapat menolak salah satunya meski perubahan mereka tidak bertumpuk.

Sifat konservatif tersebut sering dapat diterima karena aturannya sederhana. Sistem dengan contention tinggi dapat memakai aggregate yang lebih sempit, merge rule per field, atau operasi yang menyatakan intent alih-alih replacement.

Kesalahan sebaliknya lebih berbahaya: version yang hanya berubah untuk sebagian mutation dapat membiarkan writer lolos dari check setelah state relevan berubah. Setiap mutation di dalam consistency boundary yang dilindungi harus menaikkan atau mengganti token.

Partial update tetap memerlukan stale-write policy

PATCH tidak menghilangkan persoalan concurrency. Partial update tetap dapat bergantung pada representation yang dibaca sebelumnya.

Jika request bermakna “atur status menjadi approved berdasarkan record yang saya tinjau,” stale version tetap penting meski hanya status yang dikirim. Jika request bermakna “atur preference independen ini menjadi true tanpa bergantung pada field lain,” service dapat sengaja memakai kondisi yang lebih sempit.

API sebaiknya menyatakan perbedaan tersebut secara eksplisit, bukan menganggap operasi aman hanya dari jumlah field dalam payload.

Invariant multi-record memerlukan mekanisme dengan scope lebih besar

Version per row melindungi satu row dari stale replacement. Mekanisme itu sendiri tidak melindungi invariant yang mencakup beberapa row.

Misalnya, dua transaction masing-masing memeriksa record kapasitas yang berbeda lalu melakukan reservasi resource. Conditional update pada tiap row dapat sama-sama berhasil sementara hasil gabungannya melanggar batas global. Aturan seperti ini dapat memerlukan transaction, serialized coordinator, constraint, reservation protocol, atau mekanisme lain dengan scope yang mencakup invariant tersebut.

Optimistic concurrency control tepat ketika aggregate yang dilindungi juga ditentukan dengan tepat. Klaim yang melewati boundary itu hanya memberi rasa aman yang keliru.

Metric perlu memisahkan contention dari fault

Conflict merupakan hasil yang wajar ketika beberapa writer benar-benar bersaing. Kondisi ini perlu terlihat dalam observability tanpa otomatis diklasifikasikan sebagai server failure.

Signal yang berguna mencakup conflict rate per operation, jumlah attempt per mutation yang berhasil, waktu antara read dan write yang ditolak, serta jenis record atau aggregate dengan contention terkonsentrasi. Kenaikan conflict rate dapat menunjukkan resource yang baru menjadi hot, scope version yang terlalu luas, atau client menyimpan editable state lebih lama dari perkiraan.

Metric sebaiknya tidak memakai record identifier tanpa batas sebagai label. Trace atau controlled log lebih sesuai untuk identifier individual ketika diagnosis membutuhkannya.

Conditional write membuat stale state terlihat

Optimistic concurrency control tidak mencegah client membaca state lama. Mekanisme ini mencegah observasi lama diam-diam memberi izin untuk menimpa state yang lebih baru.

Boundary tersebut berguna ketika conflict mungkin terjadi tetapi menahan lock selama user berpikir, network call, atau distributed request path tidak praktis. Writer berjalan independen sampai datastore mengevaluasi kondisi pada saat mutation.

Kontrak intinya ringkas: bawa token dari read, sertakan token itu dalam atomic conditional write, perlakukan mismatch sebagai conflict kelas utama, dan lakukan retry hanya setelah operasi dinilai kembali terhadap state terbaru.