Topik temu duga representatif

Temu Duga Backend: Bagaimana Anda Mencegah Lost Update dengan Optimistic Concurrency Control?

BackendSederhana
Pasukan Editorial Offer.ccDiterbitkan Dikemas kini

Soalan

Dua pengguna sama-sama membaca versi 7 bagi satu item baris invois, kemudian menyunting dan menyimpannya. Bagaimanakah anda mereka bentuk HTTP API dan penulisan pangkalan data supaya permintaan lapuk yang terkemudian tidak dapat menimpa hasil terdahulu secara senyap?

Masalah dan senario yang berkaitan

Sumber daya invoice_items/{id} mengandungi penerangan dan jumlah. Alice dan Bob kedua-duanya membaca versi 7. Alice menyimpan dahulu dan mencipta versi 8. Bob kemudian menghantar suntingan yang masih berasaskan versi 7. Tindakan UPDATE tanpa syarat membolehkan Bob menimpa Alice sementara kedua-dua pemanggil menerima status berjaya. Itulah lost update yang mesti dicegah oleh soalan ini.

Andaikan satu HTTP JSON API, pangkalan data hubungan yang menyokong conditional update, dan ID sumber daya yang tidak pernah diguna semula. Setiap respons mempunyai satu perwakilan JSON kanonikal. Setiap perubahan medan perniagaan akan menganjakkan versi sumber daya dan menghasilkan strong ETag baharu. Konflik dijangka jarang berlaku, jadi reka bentuk ini menggunakan kawalan keserentakan optimistik berbanding memegang kunci pangkalan data semasa seseorang sedang menyunting. PUT, PATCH, dan DELETE berada dalam skop; algoritma penyuntingan kolaboratif masa nyata tidak termasuk.

Kompetensi teras adalah reka bentuk backend: menghubungkan versi yang dibaca klien, prakondisi HTTP, dan penulisan pangkalan data yang atomik ke dalam satu kontrak yang boleh diuji. Contoh menggunakan SQL gaya PostgreSQL; pangkalan data lain memerlukan conditional update dan semakan baris terjejas (affected-row) yang setara. Membandingkan versi dalam pengawal (controller) dan kemudian mengeluarkan kemas kini tanpa syarat masih meninggalkan larian perlumbaan (race condition) antara semakan dan penulisan.

Perkara yang dinilai oleh penemu duga

Isyarat pertama adalah sama ada calon boleh menunjukkan selang-seli (interleaving) lost-update dan menerangkan sebab last writer wins tidak semestinya betul. Jawapan yang mantap mengembalikan saksi versi (version witness) pada setiap bacaan yang boleh disunting dan memerlukan penulis membuktikan bahawa ia berasaskan versi semasa.

Isyarat kedua ialah semantik HTTP yang tepat. GET mengembalikan strong ETag dan permintaan yang mengubah suai menghantarnya dalam If-Match. If-Match menggunakan perbandingan kuat (strong comparison), jadi tag lapuk atau tag lemah (weak tag) tidak boleh lulus. Prakondisi wajib yang tidak disertakan boleh menghasilkan 428 Precondition Required; tag yang dibekalkan tetapi tidak sepadan menghasilkan 412 Precondition Failed. Reserve 409 Conflict untuk konflik perniagaan yang tidak dinyatakan oleh prakondisi HTTP.

Isyarat ketiga ialah menjadikan semakan dan penulisan pangkalan data sebagai atomik. Padankan kedua-dua id dan version, kemas kini medan perniagaan, dan tingkatkan versi dalam satu kenyataan. Sifar baris yang terjejas adalah titik di mana sumber daya sama ada sudah tiada atau sudah lapuk. Membaca versi dan kemudian melakukan penulisan tanpa syarat ialah larian perlumbaan TOCTOU.

Akhir sekali, perhatikan pemulihan konflik dan sempadan. Kawalan keserentakan optimistik tidak menggantikan idempotency key, dan versi satu baris tidak melindungi predikat merentas baris. Jika sumber daya yang kerap diakses (hot resource) berterusan mengembalikan 412, reka bentuk harus mengubah strategi pergeserannya dan bukannya meminta klien mencuba semula serta-merta tanpa henti.

Soalan untuk dijelaskan sebelum menjawab

  • Adakah domain konflik meliputi seluruh sumber daya atau satu medan? Versi keseluruhan sumber daya adalah paling mudah dibuktikan, tetapi

suntingan pada medan yang berbeza masih berkonflik. Konflik palsu yang kerap boleh mewajarkan agregat yang lebih kecil, API operasi eksplisit, atau peraturan penggabungan pada peringkat medan.

  • Kaedah mutasi manakah yang memerlukan versi? Soalan ini memerlukan If-Match pada PUT, PATCH, dan

DELETE. Import, tugas latar belakang, dan skrip pentadbiran mesti mengikut protokol yang sama dan bukannya mengekalkan laluan penulisan tanpa perlindungan.

  • Siapa yang menyelesaikan konflik? Untuk suntingan manusia, tunjukkan nilai asal, nilai yang dicadangkan, dan nilai semasa serta biarkan pengguna

memilih. Mesin boleh mengira semula dan mencuba lagi hanya apabila fungsi penggabungannya mengekalkan invarian perniagaan.

  • Berapakah kadar konflik dan belanjawan kependaman (latency budget)? Pertembungan yang jarang berlaku sesuai dengan reka bentuk optimistik. Rekod inventori atau

lelongan yang hangat mungkin lebih sesuai dengan satu kemas kini perniagaan atomik, transaksi pesimistik yang singkat, atau baris gilir single-writer.

  • Bolehkah klien menghantar semula permintaan yang sama selepas tamat masa? Semakan versi mengendalikan niat berbeza yang berlumba.

Idempotency key mengendalikan penghantaran semula satu niat yang sama. Reka bentuk mungkin memerlukan kedua-duanya.

  • Adakah versi ini meliputi peraturan berbilang baris? Versi baris hanya melindungi sumber daya tersebut. Peraturan seperti "sekurang-kurangnya seorang

doktor mesti kekal bersedia bertugas" memerlukan Serializable, baris kunci sepunya, atau kekangan pangkalan data.

Rangka jawapan 30 saat

"Saya akan mengembalikan sumber daya semasa dengan strong ETag, seperti \"invoice-item-42-v7\" untuk versi 7. Klien menghantar nilai tersebut dalam If-Match semasa menyimpan. Pelayan mengembalikan 428 jika pengepala wajib tiada dan 412 jika tag tersebut lapuk. Perlindungan sebenar ialah kenyataan pangkalan data yang atomik: UPDATE ... WHERE id = ? AND version = 7 menukar medan perniagaan dan meningkatkan versi secara bersama. Sifar baris bermakna data lapuk atau dipadam; jangan sekali-kali semak dahulu kemudian kemas kini tanpa syarat. Respons berjaya membawa ETag versi 8. Pada status 412, klien mengambil semula data dan membiarkan pengguna menggabungkannya. Idempotency key secara berasingan meliputi penghantaran semula selepas tamat masa. Saya akan menguji dua klien yang kedua-duanya membaca versi 7 dan membuktikan bahawa tepat satu penulisan yang berjaya."

Huraian mendalam langkah demi langkah

Mulakan dengan sejarah yang tidak betul:

text
Alice: GET item 42 -> amount=10000, version=7
Bob:   GET item 42 -> amount=10000, version=7
Alice: PUT amount=10500 -> unconditional UPDATE -> success
Bob:   PUT amount=9800  -> unconditional UPDATE -> success
Final: amount=9800; Alice's accepted update is lost

Kontrak bacaan yang disyorkan mengembalikan strong ETag. Ia adalah nilai legap (opaque value) yang mesti digemakan tepat oleh klien. Ia tidak boleh mengandungi maklumat sensitif, dan ia tidak pernah menggantikan pengesahan atau kebenaran:

http
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "invoice-item-42-v7"

{"id":42,"description":"Consulting","amountCents":10000,"version":7}

Alice meletakkan tag tersebut dalam If-Match:

http
PUT /invoice-items/42 HTTP/1.1
Content-Type: application/json
If-Match: "invoice-item-42-v7"

{"description":"Consulting","amountCents":10500}

Pelayan menjalankan pengesahan, kebenaran, dan pengesahsihan permintaan yang normal sebelum menilai prakondisi. Jika API memerlukan setiap mutasi bersyarat, If-Match yang tiada akan mengembalikan 428 dan memberitahu klien untuk mengambil semula data dan menghantar semula bersama tag. Jika nilai yang dibekalkan bukan strong ETag semasa, pelayan tidak mengubah suai sumber daya dan mengembalikan 412. Perbandingan kuat memerlukan kedua-dua entity tag bukan jenis lemah (non-weak) dan tag legapnya sepadan mengikut aksara demi aksara. Oleh itu, W/"invoice-item-42-v7" tidak boleh lulus.

Selepas semakan HTTP, pangkalan data mesti menguatkuasakan versi jangkaan yang sama secara atomik:

sql
UPDATE invoice_items
SET description = $1,
    amount_cents = $2,
    version = version + 1
WHERE id = $3
  AND version = $4
RETURNING id, description, amount_cents, version;

$4 ialah versi 7 yang dijangkakan, dinyahkod daripada ETag yang telah disahkan. Satu baris yang dikembalikan bermakna berjaya, jadi respons mengandungi versi 8 dan ETag baharunya. Jika sifar baris, baca rekod semasa dalam lingkungan sempadan kebenaran: kembalikan 404 jika ia tiada, atau 412 jika ia masih wujud. Andaian tiada penggunaan semula ID menghalang tindakan padam dan cipta semula daripada menyamar sebagai sumber daya asal. Tanpa mengira klasifikasi, laluan sifar baris tidak pernah melakukan penulisan.

Jangan jalankan SELECT version, membandingkannya, dan diikuti dengan UPDATE yang tidak mempunyai predikat versi. Transaksi lain boleh melakukan komit antara kenyataan tersebut, membolehkan permintaan yang baru lulus semakannya menimpa keadaan baharu. Walaupun pengawal telah membandingkan ETag, WHERE version = $4 ialah sempadan ketepatan muktamad. Token keserentakan ORM harus menghasilkan conditional update yang setara dan menukar sifar baris terjejas menjadi konflik keserentakan.

Selaraskan setiap status dengan puncanya:

  • 428 Precondition Required: klien meninggalkan syarat yang diperlukan oleh API ini.
  • 412 Precondition Failed: klien menghantar If-Match, tetapi perwakilan semasa tidak memenuhinya.
  • 404 Not Found: tertakluk kepada dasar kebenaran, sumber daya tidak lagi wujud.
  • 409 Conflict: prakondisi versi telah lulus, tetapi konflik keadaan perniagaan lain terpakai, seperti invois yang telah

diselesaikan dan tidak boleh diubah (immutable).

Selepas status 412, klien interaktif melakukan GET baharu dan menyimpan tiga nilai: apa yang pada asalnya dibaca oleh pengguna, apa yang dicadangkan oleh pengguna, dan apa yang disimpan oleh pelayan sekarang. Perubahan yang dicadangkan boleh digunakan secara automatik hanya pada medan yang nilai semasanya masih sama dengan nilai asal; medan lain memerlukan keputusan konflik yang eksplisit. Satu versi sumber daya secara konservatif menolak suntingan yang berasingan sekalipun. Jika itu menjadi kekangan (bottleneck) yang terukur, pecahkan sumber daya atau sediakan operasi berasaskan niat seperti POST /invoice-items/42/adjust-amount berbanding kembali secara senyap kepada pendekatan last writer wins.

Idempotency key menyelesaikan kegagalan yang berbeza. Andaikan kemas kini versi 7 oleh Alice telah dikomit sebagai versi 8 tetapi respons kejayaan telah hilang. Percubaan semula secara harfiah kini mempunyai If-Match yang lapuk. Dengan idempotency key yang stabil, pelayan boleh mengembalikan hasil pertama yang disimpan. Saksi versi mengesan dua suntingan berbeza berasaskan versi 7; rekod keidempotensian mengenali bahawa satu suntingan telah dihantar dua kali. Kedua-duanya bukan pengganti antara satu sama lain. Apabila pelayan tidak dapat membuktikan pelaksanaan terdahulu dengan pasti, mengembalikan 412 adalah lebih selamat daripada meneka kejayaan berdasarkan data semasa yang kelihatan serupa.

Kawalan keserentakan optimistik mengandaikan pergeseran yang rendah. Jika sumber daya mempunyai kadar konflik yang tinggi secara berterusan, pembacaan berulang dan penggabungan manual membazirkan kapasiti serta menjejaskan pengalaman pengguna. Penurunan inventori boleh menggunakan terus UPDATE ... SET stock = stock - $1 WHERE stock >= $1 untuk menguatkuasakan invarian satu baris. Operasi baca-putuskan-tulis (read-decide-write) yang pendek boleh menggunakan kunci baris (row lock). Agregat yang mesti memproses arahan mengikut urutan boleh dipisahkan kepada satu penulis (single-writer). Buat pilihan berdasarkan kadar konflik yang diukur, sama ada operasi bersifat kalis tukar tertib (commute), belanjawan menunggu, dan skop invarian tersebut.

Pengesahan mesti memaksa perlakuan selang-seli (interleaving) dan bukannya memanggil API dua kali secara berturutan. Minta kedua-dua klien membaca versi 7, lepaskan jumlah yang berbeza melalui penghalang (barrier), dan pastikan tepat satu kejayaan, satu 412, versi akhir 8, dan kandungan daripada pemenang. Uji juga ketiadaan If-Match yang mengembalikan 428, kegagalan tag lemah, respons kejayaan dengan ETag baharu, tag lama kekal lapuk, perlumbaan kemas kini dengan pemadaman, dan main semula dengan idempotency key yang sama selepas kehilangan respons. Dalam pengeluaran, pantau kadar missing-precondition, kadar 412, percubaan semula automatik, dan pengabaian akhir mengikut titik akhir (endpoint). Peningkatan mendadak biasanya mengenal pasti kawasan tumpuan (hotspot) atau klien yang tidak menyegarkan data.

Contoh jawapan berkualiti tinggi

"Saya akan menetapkan domain konflik kepada satu item baris invois. Kedua-dua pengguna mempunyai versi 7, jadi penyimpanan yang kemudian tidak boleh menimpa yang pertama tanpa syarat. Setiap bacaan yang boleh disunting mengembalikan strong ETag, dan setiap PUT, PATCH, atau DELETE mesti menggemakannya dalam If-Match. Pengepala yang tiada menerima 428; tag yang lapuk menerima 412 tanpa penulisan.

Membandingkan pada lapisan HTTP sahaja tidak mencukupi. Saya akan menyimpan versi bagi setiap sumber daya dan memastikan ketahanan melaksanakan UPDATE invoice_items SET ..., version = version + 1 WHERE id = ? AND version = ? RETURNING ... secara atomik. Selepas Alice berjaya dengan versi 7, baris tersebut bertukar kepada versi 8. Predikat yang sama daripada Bob menjejaskan sifar baris, jadi ia tidak boleh menimpa Alice. Kejayaan mengembalikan ETag baharu. Selepas sifar baris, bacaan yang peka kebenaran membezakan 404 daripada 412, tetapi tiada cabang yang kembali kepada kemas kini tanpa syarat.

Pada status 412, klien mengambil semula dan membandingkan nilai asal, dicadangkan, dan semasa supaya pengguna dapat menyelesaikan konflik sebenar. Jika medan yang berasingan sering bertembung, saya akan memecahkan sumber daya atau mereka bentuk operasi atomik berasaskan niat. Percubaan semula akibat tamat masa secara berasingan menggunakan idempotency key: ia mengenali penghantaran pendua bagi satu niat, manakala versi mengenali niat berbeza berdasarkan keadaan lama yang sama.

Saya akan menggunakan dua sambungan dan penghalang (barrier) supaya kedua-duanya membaca versi 7 sebelum menghantar. Tepat satu mesti berjaya, yang satu lagi mesti menerima 412, dan versi akhir mestilah 8. Saya juga akan menguji ketiadaan If-Match, tag lemah, perlumbaan padam, dan main semula bagi respons yang hilang. Jika kadar 412 kekal tinggi dalam pengeluaran, saya akan menilai kemas kini perniagaan bersyarat, kunci pendek, atau single writer daripada meningkatkan percubaan semula tanpa had."

Kesilapan lazim

  • Menyemak versi dalam kod aplikasi sebelum mengemas kini → komit lain boleh berlaku antara semakan dan

penulisan → letakkan versi dalam predikat UPDATE yang sama dan periksa baris yang terjejas.

  • Meneruskan apabila If-Match tiada → klien yang dilindungi wujud bersama klien lama yang masih boleh merosakkan

data → jadikan mutasi bersyarat sebahagian daripada kontrak API dan kembalikan 428 apabila ia tiada.

  • Menerima weak ETag untuk If-Match piawaian memerlukan perbandingan kuat, jadi tag lemah tidak boleh sepadan

janakan strong ETag yang sah secara semantik untuk perwakilan yang boleh disunting.

  • Mengembalikan 409 untuk setiap konflik → klien tidak dapat membezakan kegagalan prakondisi HTTP daripada konflik

keadaan perniagaan → kembalikan 412 untuk prakondisi versi yang gagal dan khaskan 409 untuk konflik perniagaan yang bebas.

  • Menghantar semula permintaan yang sama secara automatik selepas 412 → tag lama kekal lapuk, manakala menggantikannya secara senyap

boleh menimpa suntingan lain → ambil semula dan kira semula atau minta pengguna untuk menggabungkannya.

  • Mengandaikan idempotency key menghalang penindihan serentak → dua suntingan berbeza menggunakan kunci berbeza tetapi masih

boleh berkongsi satu pangkalan lapuk yang sama → **gunakan syarat keidempotensian dan versi masing-masing untuk penghantaran pendua dan niat serentak.**

  • Mendakwa versi pada setiap baris melindungi setiap invarian → penyelewengan tulis (write skew) merentas baris tidak pernah berkonflik pada versi satu

baris → pilih Serializable, titik pergeseran sepunya, atau kekangan bagi skop invarian tersebut.

  • Mencuba semula hotspot serta-merta tanpa had → permintaan yang gagal akan bertembung lagi dan meningkatkan beban → **ukur

konflik dan pertimbangkan operasi atomik, kunci pendek, atau single writer.**

Soalan susulan dan respons

Susulan 1: Patutkah dua permintaan PATCH ke medan yang berbeza berkonflik?

Versi keseluruhan sumber daya berubah pada sebarang pengubahsuaian medan, jadi tampalan yang berasingan akan berkonflik. Itu adalah tetapan lalai yang selamat dan mudah diterangkan. Jika bukti menunjukkan banyak konflik palsu, pecahkan kitaran hayat yang berubah secara bebas kepada subsumber daya atau hantar garis dasar medan dan tentukan penggabungan tiga hala (three-way merge). Token peringkat medan mengurangkan konflik palsu tetapi menggandakan keadaan token serta merumitkan invarian majmuk dan pengauditan; jangan lakukannya semata-mata untuk menindas respons 412.

Susulan 2: Apakah yang berlaku apabila klien mencuba semula selepas tamat masa dan ETag lama sudah lapuk?

Hantar idempotency key yang stabil dan simpan kunci, ringkasan permintaan (request digest), versi yang dijangkakan, dan hasil pertama bersama-sama. Main semula dengan kunci dan permintaan yang sama mengembalikan hasil pertama; kunci yang sama dengan permintaan berbeza akan ditolak. Tanpa rekod keidempotensian yang boleh dipercayai, medan semasa yang hanya kelihatan serupa tidak membuktikan bahawa permintaan pertama telah berjaya. Kembalikan 412 dan biarkan klien memeriksa keadaan semasa.

Susulan 3: Bolehkah updated_at menjadi token versi?

Ia selamat hanya jika pangkalan data yang menjanakannya, setiap mutasi yang berkaitan mengubahnya, kepersisannya membezakan penulisan berturut-turut, dan setiap nod memberikannya semantik yang sama. Kepersisan cap masa yang terpotong atau laluan penulisan pintasan boleh memberikan nilai yang sama kepada dua versi. Integer bagi setiap sumber daya atau versi baris asli pangkalan data secara umumnya memberikan semakan kesaksamaan yang kurang kabur dan masih boleh dikodkan sebagai ETag legap.

Susulan 4: Bagaimanakah reka bentuk harus berubah di bawah pergeseran yang tinggi?

Mula-mula kelompokkan respons 412 mengikut sumber daya dan operasi untuk mengasingkan antara satu kunci panas, domain konflik yang terlalu luas, dan klien yang lama di luar talian. Tukarkan niat penambahan atau pengurangan kalis tukar tertib kepada ungkapan pangkalan data yang atomik. Gunakan kunci baris untuk keputusan bukan kalis tukar tertib yang pendek, atau halakan arahan yang tersusun rapi mengikut kunci sumber daya kepada satu penulis. Setiap alternatif mengimbangi masa menunggu, pemprosesan (throughput), atau kerumitan seni bina demi mengurangkan penulisan yang ditolak, jadi data konflik dan kependaman yang diukur harus menjadi pencetus perubahan.

Susulan 5: Mengapakah versi satu baris tidak menghalang penyelewengan tulis (write skew)?

Dalam penyelewengan tulis, dua transaksi membaca predikat merentas baris yang sama dan mengemas kini rekod yang berbeza, jadi kedua-dua syarat versi baris boleh lulus. Setiap token hanya membuktikan bahawa baris yang dikemas kini belum berubah; ia tidak menyatakan apa-apa tentang predikat yang dikongsi. Unjurkan invarian ke baris pembilang sepunya, kunci kawalan sepunya, tambah kekangan pangkalan data yang sesuai, atau gunakan Serializable untuk mengesan kebergantungan yang tidak boleh disiri (non-serializable).

Sumber awam

Soalan berkaitan