Topik wawancara representatif

Wawancara Backend: Bagaimana Cara Mencegah Lost Update dengan Optimistic Concurrency Control?

BackendSedang
Tim Redaksi Offer.ccDipublikasikan Diperbarui

Pertanyaan

Dua pengguna sama-sama membaca versi 7 dari sebuah item baris faktur, lalu mengedit dan menyimpannya. Bagaimana Anda merancang HTTP API dan penulisan basis data agar permintaan basi yang datang belakangan tidak dapat menimpa hasil sebelumnya secara diam-diam?

Masalah dan skenario yang berlaku

Sumber daya invoice_items/{id} memuat deskripsi dan jumlah. Alice dan Bob sama-sama membaca versi 7. Alice menyimpan lebih dulu dan menghasilkan versi 8. Bob kemudian mengirimkan editan yang masih berbasis pada versi 7. UPDATE tanpa syarat memungkinkan Bob menimpa perubahan Alice sementara kedua pemanggil menerima status sukses. Itulah lost update yang harus dicegah oleh pertanyaan ini.

Asumsikan sebuah HTTP JSON API, basis data relasional yang mendukung conditional update, dan ID sumber daya yang tidak pernah digunakan kembali. Setiap respons memiliki satu representasi JSON kanonikal. Setiap perubahan pada field bisnis akan memajukan versi sumber daya dan menghasilkan strong ETag baru. Konflik diperkirakan jarang terjadi, sehingga desain ini menggunakan konkurensi optimistik alih-alih menahan kunci basis data saat seseorang sedang mengedit. PUT, PATCH, dan DELETE masuk dalam cakupan; algoritma pengeditan kolaboratif real-time berada di luar cakupan.

Kompetensi intinya adalah desain backend: menghubungkan versi yang dibaca klien, prakondisi HTTP, dan penulisan basis data yang atomik ke dalam satu kontrak yang dapat diuji. Contoh-contoh menggunakan SQL bergaya PostgreSQL; basis data lain membutuhkan conditional update dan pemeriksaan baris terpengaruh (affected-row) yang setara. Membandingkan versi di controller lalu menjalankan pembaruan tanpa syarat tetap menyisakan kondisi race condition antara pemeriksaan dan penulisan.

Hal yang dievaluasi oleh pewawancara

Sinyal pertama adalah apakah kandidat dapat menunjukkan interleaving dari lost-update dan menjelaskan mengapa last writer wins belum tentu benar. Jawaban yang kuat mengembalikan bukti versi (version witness) pada setiap pembacaan yang dapat diedit dan mewajibkan penulis untuk membuktikan bahwa perubahan tersebut didasarkan pada versi terkini.

Sinyal kedua adalah semantik HTTP yang presisi. Permintaan GET mengembalikan strong ETag dan permintaan mutasi mengirimkannya dalam If-Match. If-Match menggunakan perbandingan kuat (strong comparison), sehingga tag basi atau tag lemah (weak tag) tidak dapat lolos. Prakondisi wajib yang tidak disertakan dapat menghasilkan 428 Precondition Required; tag yang diberikan namun tidak cocok menghasilkan 412 Precondition Failed. Reserve 409 Conflict untuk konflik bisnis yang tidak diekspresikan oleh prakondisi HTTP.

Sinyal ketiga adalah membuat pemeriksaan dan penulisan basis data menjadi atomik. Cocokkan id dan version, perbarui field bisnis, dan naikkan versi dalam satu pernyataan. Nol baris terpengaruh adalah titik di mana sumber daya sudah tidak ada atau sudah basi. Membaca versi lalu melakukan penulisan tanpa syarat adalah TOCTOU race.

Terakhir, perhatikan pemulihan konflik dan batasannya. Konkurensi optimistik tidak menggantikan idempotency key, dan versi satu baris tidak melindungi predikat lintas-baris. Jika sebuah sumber daya yang sangat aktif (hot resource) terus-menerus mengembalikan 412, desain harus mengubah strategi kontensinya alih-alih membuat klien mencoba ulang secara langsung selamanya.

Pertanyaan untuk diklarifikasi sebelum menjawab

  • Apakah domain konflik mencakup seluruh sumber daya atau satu field? Versi seluruh sumber daya paling mudah dibuktikan, namun

editan pada field yang berbeda akan tetap berkonflik. Konflik semu yang sering terjadi dapat menjadi alasan untuk menggunakan agregat yang lebih kecil, API operasi eksplisit, atau aturan penggabungan tingkat field.

  • Metode mutasi mana saja yang memerlukan versi? Pertanyaan ini mewajibkan If-Match pada PUT, PATCH, dan

DELETE. Proses impor, background job, dan skrip administrasi harus mengikuti protokol yang sama alih-alih mempertahankan jalur penulisan tanpa perlindungan.

  • Siapa yang menyelesaikan konflik? Untuk editan manusia, tampilkan nilai asli, nilai yang diusulkan, dan nilai saat ini lalu biarkan pengguna

memilih. Mesin boleh menghitung ulang dan mencoba lagi hanya jika fungsi penggabungannya mempertahankan invarian bisnis.

  • Berapa tingkat konflik dan batasan latensi? Tabrakan yang jarang cocok dengan desain optimistik. Data inventaris yang panas atau

catatan lelang mungkin lebih cocok menggunakan satu pembaruan bisnis atomik, transaksi pesimistik singkat, atau antrean single-writer.

  • Bisakah klien mengirim ulang permintaan yang sama setelah timeout? Pemeriksaan versi menangani maksud berbeda yang saling berlomba.

Idempotency key menangani pengiriman ulang dari satu maksud yang sama. Desain mungkin memerlukan keduanya.

  • Apakah versi mencakup aturan multi-baris? Versi baris hanya melindungi sumber daya tersebut. Aturan seperti "satu

dokter harus tetap bertugas" memerlukan Serializable, baris kunci bersama, atau batasan (constraint) basis data.

Kerangka jawaban 30 detik

"Saya akan mengembalikan sumber daya saat ini dengan strong ETag, seperti \"invoice-item-42-v7\" untuk versi 7. Klien mengirimkan nilai tersebut dalam If-Match saat menyimpan. Server mengembalikan 428 jika header wajib tidak ada dan 412 jika tag tersebut basi. Perlindungan sebenarnya adalah pernyataan basis data yang atomik: UPDATE ... WHERE id = ? AND version = 7 mengubah field bisnis dan menaikkan versi secara bersamaan. Nol baris berarti data basi atau telah dihapus; jangan pernah memeriksa terlebih dahulu lalu memperbarui tanpa syarat. Respons sukses membawa ETag versi 8. Pada status 412, klien mengambil ulang data dan membiarkan pengguna menggabungkannya. Idempotency key secara terpisah menangani transmisi ulang setelah timeout. Saya akan menguji dua klien yang sama-sama membaca versi 7 dan membuktikan bahwa tepat satu penulisan yang berhasil."

Pembahasan mendalam langkah demi langkah

Mari mulai dengan riwayat yang salah:

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 pembacaan yang direkomendasikan mengembalikan strong ETag. Ini adalah nilai buram (opaque value) yang harus dikirimkan kembali secara persis oleh klien. Nilai ini tidak boleh memuat informasi sensitif, dan tidak pernah menggantikan autentikasi atau otorisasi:

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

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

Alice menyertakan tag tersebut di If-Match:

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

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

Server menjalankan autentikasi, otorisasi, dan validasi permintaan seperti biasa sebelum mengevaluasi prakondisi. Jika API mewajibkan setiap mutasi bersifat kondisional, If-Match yang tidak disertakan akan mengembalikan 428 dan memberi tahu klien untuk mengambil ulang data dan mengirimkan ulang bersama sebuah tag. Jika nilai yang diberikan bukan merupakan strong ETag saat ini, server tidak mengubah sumber daya dan mengembalikan 412. Perbandingan kuat mengharuskan kedua entity tag bersifat non-weak dan tag buramnya cocok karakter demi karakter. Oleh karena itu, W/"invoice-item-42-v7" tidak dapat lolos.

Setelah pemeriksaan HTTP, basis data harus menerapkan versi yang diharapkan 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 adalah versi 7 yang diharapkan, yang didekodekan dari ETag yang telah divalidasi. Satu baris yang dikembalikan berarti sukses, sehingga respons memuat versi 8 dan ETag barunya. Jika nol baris, baca data saat ini dalam batasan otorisasi: kembalikan 404 jika data tidak ada, atau 412 jika data masih ada. Asumsi tidak adanya penggunaan ulang ID mencegah skenario hapus dan buat ulang menyamar sebagai sumber daya asli. Terlepas dari klasifikasinya, jalur nol baris tidak pernah melakukan penulisan.

Jangan menjalankan SELECT version, membandingkannya, lalu melanjutkannya dengan UPDATE yang tidak memiliki predikat versi. Transaksi lain dapat melakukan commit di antara pernyataan-pernyataan tersebut, memungkinkan permintaan yang baru saja lolos pemeriksaannya untuk menimpa state baru. Meskipun controller membandingkan ETag, WHERE version = $4 adalah batas kebenaran akhir. Token konkurensi ORM harus menghasilkan conditional update yang setara dan mengubah nol baris terpengaruh menjadi konflik konkurensi.

Sesuaikan setiap status dengan penyebabnya:

  • 428 Precondition Required: klien menghilangkan kondisi yang diwajibkan oleh API ini.
  • 412 Precondition Failed: klien mengirimkan If-Match, namun representasi saat ini tidak memenuhinya.
  • 404 Not Found: bergantung pada kebijakan otorisasi, sumber daya sudah tidak ada.
  • 409 Conflict: prakondisi versi lolos, tetapi konflik business-state lain berlaku, seperti faktur yang telah

lunas sehingga tidak dapat diubah (immutable).

Setelah status 412, klien interaktif melakukan GET baru dan menyimpan tiga nilai: apa yang awalnya dibaca pengguna, apa yang diusulkan pengguna, dan apa yang sekarang disimpan oleh server. Perubahan yang diusulkan dapat diterapkan secara otomatis hanya pada field yang nilai saat ininya masih sama dengan nilai asli; field lainnya memerlukan keputusan konflik yang eksplisit. Satu versi sumber daya secara konservatif menolak pengeditan yang terpisah sekalipun. Jika hal tersebut terbukti menjadi bottleneck yang terukur, pecah sumber daya atau sediakan operasi berbasis niat (intent-based) seperti POST /invoice-items/42/adjust-amount daripada kembali secara diam-diam ke pendekatan last writer wins.

Idempotency key menyelesaikan kegagalan yang berbeda. Misalkan pembaruan versi 7 oleh Alice berhasil di-commit sebagai versi 8 tetapi respons sukses hilang dalam perjalanan. Pengiriman ulang secara harfiah kini memiliki If-Match yang basi. Dengan idempotency key yang stabil, server dapat mengembalikan hasil pertama yang telah disimpan. Bukti versi (version witness) mendeteksi dua pengeditan berbeda yang berbasis pada versi 7; catatan idempotensi mengenali bahwa satu pengeditan dikirimkan dua kali. Keduanya bukan pengganti satu sama lain. Ketika server tidak dapat membuktikan eksekusi sebelumnya secara andal, mengembalikan 412 lebih aman daripada menebak keberhasilan dari data saat ini yang serupa.

Konkurensi optimistik mengasumsikan kontensi yang rendah. Jika suatu sumber daya terus-menerus memiliki tingkat konflik yang tinggi, pembacaan berulang dan penggabungan manual akan membuang-buang kapasitas dan memperburuk pengalaman pengguna. Pengurangan inventaris dapat langsung menggunakan UPDATE ... SET stock = stock - $1 WHERE stock >= $1 untuk menegakkan invarian satu baris. Operasi read-decide-write yang singkat dapat menggunakan row lock. Agregat yang harus memproses perintah secara berurutan dapat dipartisi ke satu penulis (single-writer). Pilihlah berdasarkan tingkat konflik yang terukur, apakah operasi bersifat komutatif, batasan waktu tunggu, dan cakupan dari invarian tersebut.

Verifikasi harus memaksakan interleaving daripada memanggil API dua kali secara berurutan. Buat kedua klien membaca versi 7, kirimkan nilai jumlah yang berbeda melalui pembatas (barrier), dan pastikan tepat satu yang sukses, satu 412, versi akhir 8, dan konten berasal dari pemenang. Uji juga kondisi hilangnya If-Match yang mengembalikan 428, weak tag yang gagal, respons sukses dengan ETag baru, tag lama yang tetap basi, race condition antara update dan delete, serta replay dengan idempotency key yang sama setelah respons hilang. Di lingkungan produksi, pantau tingkat missing-precondition, tingkat 412, retry otomatis, dan pengabaian akhir berdasarkan endpoint. Lonjakan tiba-tiba biasanya mengidentifikasi hotspot atau klien yang tidak memperbarui datanya.

Contoh jawaban berkualitas tinggi

"Saya akan menetapkan domain konflik pada satu item baris faktur. Kedua pengguna memiliki versi 7, sehingga penyimpanan yang lebih lambat tidak boleh menimpa yang pertama tanpa syarat. Setiap pembacaan yang dapat diedit mengembalikan strong ETag, dan setiap PUT, PATCH, atau DELETE harus mencantumkannya kembali di If-Match. Header yang hilang menghasilkan 428; tag yang basi menghasilkan 412 tanpa penulisan.

Membandingkan pada lapisan HTTP saja tidak cukup. Saya akan menyimpan versi per sumber daya dan membuat persistensi mengeksekusi UPDATE invoice_items SET ..., version = version + 1 WHERE id = ? AND version = ? RETURNING ... secara atomik. Setelah Alice berhasil dengan versi 7, baris tersebut menjadi versi 8. Predikat yang sama dari Bob akan memengaruhi nol baris, sehingga tidak dapat menimpa perubahan Alice. Keberhasilan mengembalikan ETag baru. Setelah nol baris, pembacaan yang memperhatikan otorisasi membedakan 404 dari 412, tetapi tidak ada cabang yang kembali ke pembaruan tanpa syarat.

Pada status 412, klien mengambil ulang data dan membandingkan nilai asli, yang diusulkan, dan nilai saat ini sehingga pengguna dapat menyelesaikan konflik yang nyata. Jika field yang tidak terkait sering bertabrakan, saya akan memecah sumber daya atau merancang operasi atomik berbasis niat. Retry akibat timeout secara terpisah menggunakan idempotency key: ini mengenali duplikasi transmisi dari satu niat yang sama, sedangkan versi mengenali niat berbeda yang didasarkan pada state lama yang sama.

Saya akan menggunakan dua koneksi dan sebuah barrier sehingga keduanya membaca versi 7 sebelum mengirimkan data. Tepat satu yang harus berhasil, yang lain harus menerima 412, dan versi akhir harus 8. Saya juga akan menguji ketiadaan If-Match, weak tag, delete race, dan replay saat respons hilang. Jika tingkat 412 tetap tinggi di produksi, saya akan mengevaluasi conditional business update, penguncian singkat, atau single writer daripada meningkatkan retry tanpa batas."

Kesalahan umum

  • Memeriksa versi di kode aplikasi sebelum memperbarui → commit lain dapat terjadi di antara pemeriksaan dan

penulisan → masukkan versi ke dalam predikat UPDATE yang sama dan periksa baris yang terpengaruh.

  • Melanjutkan proses saat If-Match tidak ada → klien yang terlindungi hidup berdampingan dengan klien lama yang masih dapat merusak

data → jadikan mutasi bersyarat sebagai bagian dari kontrak API dan kembalikan 428 jika header tersebut tidak ada.

  • Menerima weak ETag untuk If-Match standar mewajibkan perbandingan kuat, sehingga weak tag tidak dapat cocok

hasilkan strong ETag yang valid secara semantik untuk representasi yang dapat diedit.

  • Mengembalikan 409 untuk setiap konflik → klien tidak dapat membedakan kegagalan prakondisi HTTP dari konflik

keadaan bisnis → kembalikan 412 untuk kegagalan prakondisi versi dan sediakan 409 untuk konflik bisnis yang independen.

  • Mengirim ulang permintaan yang sama secara otomatis setelah 412 → tag lama tetap basi, sedangkan menggantinya secara diam-diam

dapat menimpa editan lain → ambil ulang dan hitung ulang atau minta pengguna untuk menggabungkannya.

  • Mengasumsikan idempotency key mencegah penimpaan konkuren → dua editan berbeda menggunakan key berbeda tetapi masih

dapat berbagi satu basis yang basi → **gunakan kondisi idempotensi dan versi masing-masing untuk transmisi duplikat dan niat konkuren.**

  • Mengklaim bahwa versi pada setiap baris melindungi setiap invarian → write skew lintas-baris tidak pernah berkonflik pada versi satu

baris → pilih Serializable, titik kontensi bersama, atau constraint untuk cakupan invarian tersebut.

  • Mencoba ulang hotspot secara langsung tanpa batas → permintaan yang gagal akan bertabrakan lagi dan melipatgandakan beban → **ukur

konflik dan pertimbangkan operasi atomik, penguncian singkat, atau single writer.**

Pertanyaan lanjutan dan jawabannya

Lanjutan 1: Haruskah dua permintaan PATCH ke field berbeda berkonflik?

Versi seluruh sumber daya akan bertambah pada perubahan field apa pun, sehingga patch yang tidak bersinggungan tetap berkonflik. Itu adalah default yang aman dan mudah dijelaskan. Jika bukti menunjukkan banyak konflik semu, pisahkan siklus hidup yang berubah secara independen ke dalam subsumber daya atau kirimkan baseline field dan tentukan three-way merge. Token tingkat field mengurangi konflik semu tetapi melipatgandakan state token dan memperumit invarian gabungan serta audit; jangan menambahkannya hanya untuk menekan respons 412.

Lanjutan 2: Apa yang terjadi ketika klien mencoba ulang setelah timeout dan ETag lama sudah basi?

Kirimkan idempotency key yang stabil dan simpan key, ringkasan permintaan (request digest), versi yang diharapkan, dan hasil pertama secara bersamaan. Pengiriman ulang dengan key dan permintaan yang sama akan mengembalikan hasil pertama; key yang sama dengan permintaan berbeda akan ditolak. Tanpa catatan idempotensi yang andal, field saat ini yang hanya terlihat serupa tidak membuktikan bahwa permintaan pertama telah berhasil. Kembalikan 412 dan biarkan klien memeriksa status terkini.

Lanjutan 3: Bisakah updated_at menjadi token versi?

Ini aman hanya jika basis data yang menghasilkannya, setiap mutasi relevan mengubahnya, tingkat presisinya membedakan penulisan berturut-turut, dan setiap node memberikannya semantik yang sama. Presisi timestamp yang terpotong atau jalur penulisan yang memintas dapat memberikan nilai yang sama pada dua versi. Integer per sumber daya atau versi baris bawaan basis data umumnya memberikan pemeriksaan kesetaraan yang tidak ambigu dan masih dapat dienkode sebagai ETag buram.

Lanjutan 4: Bagaimana desain harus berubah di bawah kontensi tinggi?

Pertama, kelompokkan respons 412 berdasarkan sumber daya dan operasi untuk memisahkan antara satu kunci yang panas, domain konflik yang terlalu luas, dan klien yang lama offline. Ubah niat inkremen atau dekremen komutatif menjadi ekspresi basis data yang atomik. Gunakan row lock untuk keputusan non-komutatif yang singkat, atau rutekan perintah yang berurutan ketat berdasarkan kunci sumber daya ke satu penulis. Setiap alternatif mengorbankan waktu tunggu, throughput, atau kompleksitas arsitektur untuk mengurangi penulisan yang ditolak, sehingga data konflik dan latensi yang terukur harus menjadi pemicu perubahan tersebut.

Lanjutan 5: Mengapa versi satu baris tidak mencegah write skew?

Pada write skew, dua transaksi membaca predikat lintas-baris yang sama dan memperbarui catatan yang berbeda, sehingga kondisi versi baris pada keduanya dapat lolos. Setiap token hanya membuktikan bahwa baris yang sedang diperbarui belum berubah; token tersebut tidak mengatakan apa pun tentang predikat bersama. Petakan invarian ke baris counter bersama, kunci pelindung bersama, tambahkan batasan basis data yang sesuai, atau gunakan Serializable untuk mendeteksi dependensi non-serializable.

Sumber publik

Pertanyaan terkait