Topik temu duga representatif

Reka Bentuk API Cipta Pesanan yang Idempoten

BackendSukar
Pasukan Editorial Offer.ccDiterbitkan Dikemas kini

Soalan

Reka bentuk kontrak keidempotennan untuk POST /orders: klien mesti boleh mencuba semula selepas masa tamat (timeout), penduaan serentak hanya boleh mencipta satu pesanan, penggunaan semula kunci dengan parameter berbeza mesti ditolak, dan reka bentuk ini mesti mengendalikan respons yang hilang selepas komit, operasi yang sedang berjalan, serta kesan sampingan hiliran.

Masalah dan Skop

Reka bentuk kontrak keidempotennan untuk POST /orders. Klien mudah alih mungkin mengalami masa tamat (timeout) pada rangkaian yang tidak stabil dan mencuba semula secara automatik. Get laluan (gateway) juga mungkin memainkan semula permintaan selepas sambungannya terputus. Sama ada satu operasi logik tiba sekali atau beberapa kali, pelayan hanya boleh mencipta satu pesanan. Selepas permintaan berjaya, percubaan semula yang seiras harus memainkan semula hasil yang pertama; pelayan mesti menolak kunci keidempotennan yang sama apabila ia mewakili parameter pesanan yang berbeza.

Andaikan pemanggil menghantar pengecam rawak untuk satu operasi logik dalam Idempotency-Key, dan pelayan menggunakan PostgreSQL. Penulisan pesanan dan peristiwa yang belum selesai boleh berkongsi satu transaksi pangkalan data tempatan. Sistem pembayaran dan inventori luaran tidak boleh mengambil bahagian dalam transaksi tersebut. API ini bersifat berbilang penyewa (multi-tenant), dan responsnya boleh mengandungi data pesanan yang memerlukan kebenaran (authorization).

Skop ini merangkumi penyahaslian penduaan permintaan (request deduplication), perlumbaan serentak (concurrent races), sempadan transaksi, main semula hasil, pengekalan (retention), dan kesan sampingan hiliran. Replikasi pangkalan data rentas wilayah, mesin keadaan domain pembayaran itu sendiri, dan pemilihan broker mesej berada di luar skop. Ini adalah soalan bahagian belakang (backend) kerana tugas utamanya adalah menukar semantik API kepada kekangan pangkalan data, pemulihan kegagalan, dan aliran permintaan yang boleh dicerap, dan bukannya mereka bentuk sistem pelbagai komponen yang luas.

Perkara yang Dinilai oleh Penemu Duga

Isyarat pertama ialah sama ada calon memisahkan identiti percubaan semula daripada identiti perniagaan. Kunci keidempotennan mengenal pasti satu operasi dari perspektif pemanggil. Ia tidak menggantikan pengesahan (authentication), pengasingan penyewa, atau kunci pesanan peringkat domain. Pelayan harus mengehadkan skopnya sekurang-kurangnya mengikut (tenant_id, endpoint, idempotency_key). Carian menggunakan kunci semata-mata boleh membolehkan dua penyewa yang kebetulan memilih rentetan yang sama membaca hasil antara satu sama lain.

Isyarat kedua ialah sama ada ketepatan keserentakan bergantung pada kekangan keunikan dan bukannya pemasaan aplikasi. SELECT yang diikuti oleh INSERT membolehkan dua permintaan melihat tiada baris dan kedua-duanya bertindak sebagai permintaan pertama. Indeks unik dengan INSERT ... ON CONFLICT membolehkan pangkalan data memilih satu pemilik. Perbandingan tersebut juga mesti merangkumi cap jari permintaan; jika tidak, penggunaan semula kunci lama secara tidak sengaja boleh mengembalikan pesanan lama sebagai hasil daripada permintaan pesanan yang berbeza.

Isyarat ketiga ialah sempadan komit yang tepat. Pesanan, hasil keidempotennan, dan peristiwa outbox mesti dikomit bersama-sama. Kegagalan sebelum komit akan membalikkan (rollback) ketiga-tiganya. Jika komit berjaya tetapi respons HTTP hilang, percubaan semula membaca rekod yang telah selesai dan memainkannya semula. Panggilan pembayaran dan inventori berlaku di luar transaksi melalui pengguna outbox, yang menghantar pengecam keidempotennan terbitan ke hiliran.

Jawapan yang kukuh juga mentakrifkan pengekalan, respons kepada pendua serentak, jenis kegagalan yang disimpan, cara operasi yang berjalan lama dipulihkan, dan cara konflik kunci serta tugas yang tersekat dipantau. “Gunakan kunci Redis” membiarkan isu tamat tempoh kunci, ranap proses, main semula hasil, dan perlumbaan selepas komit pangkalan data tidak terjawab.

Soalan untuk Dijelaskan Sebelum Menjawab

  • Apakah yang dikira sebagai pendua? Di sini, penyewa, titik akhir, dan kunci keidempotennan yang sama mengenal pasti satu operasi. Jika domain sudah mempunyai checkout_id yang tidak boleh digunakan semula, jadual pesanan juga harus mempunyai kekangan keunikan bebas ke atasnya.
  • Bolehkah pemanggil memilih ID sumber? Jika pemanggil memiliki ID pesanan yang stabil, PUT /orders/{clientOrderId} adalah alternatif kerana HTTP mentakrifkan PUT sebagai idempoten. POST dengan kunci keidempotennan sesuai untuk ID pesanan yang dijana oleh pelayan.
  • Adakah penciptaan pesanan sepenuhnya merupakan kerja pangkalan data? Jika ia muat dalam satu transaksi pendek, gunakan reka bentuk transaksi tunggal. Aliran kerja yang mengambil masa berpuluh-puluh saat memerlukan rekod IN_PROGRESS yang kelihatan, pajakan (lease), dan token pengambilalihan dan bukannya transaksi pangkalan data yang dipegang lama.
  • Berapa tepatkah main semula tersebut? Reka bentuk ini menyimpan status HTTP pertama dan badan respons yang selamat untuk dimainkan semula. Jika respons mengandungi tandatangan jangka pendek atau medan langsung, simpan ID sumber dan bina semula respons di bawah kontrak yang eksplisit.
  • Berapa lama kunci dikekalkan? Pengekalan mesti meliputi tetingkap percubaan semula atau main semula luar talian terpanjang bagi klien. Jika penduaan mesti kekal mustahil selepas tetingkap tersebut, tambahkan kunci domain kekal; TTL cache yang lebih lama bukan pengganti kepada kekangan perniagaan.
  • Kegagalan manakah yang harus diingati? Pengesahan yang gagal sebelum pemilikan diperoleh tidak disimpan. Penolakan perniagaan deterministik di dalam transaksi boleh disimpan dan dimainkan semula. Kegagalan infrastruktur yang membalikkan transaksi tidak meninggalkan hasil yang selesai, jadi klien boleh mencuba semula.

Jawapan 30 Saat

“Saya akan mengehadkan skop setiap kunci keidempotennan kepada penyewa dan POST /orders, kemudian mengambil cap jari parameter pesanan yang telah dinormalkan. Kekangan unik pada (tenant_id, endpoint, idempotency_key) mengadili permintaan serentak. Permintaan pertama menulis rekod keidempotennan, pesanan, dan peristiwa outbox dalam satu transaksi, kemudian menyimpan status dan badan respons sebelum komit. Percubaan semula dengan cap jari yang sama memainkan semula hasil tersebut; cap jari yang berbeza mengembalikan konflik. Oleh itu, respons yang hilang selepas komit tidak boleh mencipta pesanan lain. Pembayaran dan inventori berjalan secara tak segerak daripada outbox dengan kunci keidempotennan terbitan. Saya akan mengekalkan rekod permintaan untuk tetingkap percubaan semula maksimum dan menggunakan kekangan keunikan domain pesanan yang berasingan untuk penyahaslian penduaan kekal.”

Penyelaman Mendalam Langkah demi Langkah

Mulakan dengan protokol. Pemanggil yang disahkan menjana kunci berentropi tinggi untuk satu niat “cipta pesanan” dan menggunakannya semula untuk setiap percubaan semula. Pesanan baharu memerlukan kunci baharu. Kunci tersebut tidak boleh mengandungi alamat e-mel, nombor telefon, atau data sensitif lain. Pelayan mengehadkan panjang dan set aksaranya, tetapi kunci yang kelihatan rawak tidak pernah menjadi kelayakan kebenaran. POST tidak mempunyai semantik idempoten PUT secara lalai; percubaan semula yang selamat datang daripada kontrak aplikasi ini.

Bina cap jari permintaan daripada medan perniagaan yang disahkan oleh pelayan, bukan bait JSON mentah. Susunan medan, ruang putih yang tidak penting, dan nilai lalai tidak sepatutnya memberikan cap jari yang berbeza kepada permintaan yang sama. Kecualikan ID surih (trace ID), cap masa, dan medan bukan perniagaan yang lain. Input praktikal ialah pengekodan stabil bagi DTO yang dinormalkan, versi titik akhir, dan setiap medan yang mempengaruhi hasil pesanan. Cincang (hash) pengekodan tersebut. Ringkasan (digest) mengesan penyalahgunaan kunci; ia bukan tandatangan atau mekanisme pengesahan.

Skema minimum ini menyatakan kekangan yang diperlukan; SQL ini adalah untuk tujuan ilustrasi:

sql
CREATE TABLE idempotency_requests (
  tenant_id text NOT NULL,
  endpoint text NOT NULL,
  idempotency_key text NOT NULL,
  request_fingerprint text NOT NULL,
  response_status integer,
  response_body jsonb,
  resource_id uuid,
  created_at timestamptz NOT NULL,
  expires_at timestamptz NOT NULL,
  PRIMARY KEY (tenant_id, endpoint, idempotency_key)
);

Laluan biasa untuk transaksi pendek ialah:

  1. Di luar transaksi, sahkan identiti, sahkan format kunci dan permintaan, dan kira cap jari.
  2. Mulakan transaksi dan cuba tuntut kunci dengan INSERT ... ON CONFLICT DO NOTHING RETURNING ....
  3. Permintaan yang menyisipkan baris ialah pemiliknya. Ia mencipta pesanan, menambah peristiwa outbox, mengemas kini baris keidempotennan dengan status, badan respons, dan ID pesanan, serta mengomit sekali.
  4. Permintaan yang tidak menyisipkan akan membaca baris sedia ada. Cap jari yang berbeza mengembalikan 409 idempotency_key_reused. Cap jari yang sama mengembalikan status dan badan tersimpan yang pertama, secara pilihan dengan penanda main semula yang ditakrifkan oleh API.
  5. Hantar respons HTTP hanya selepas komit. Jika sambungan terputus selepas komit, permintaan seterusnya mengikut langkah 4 dan bukannya mencipta pesanan.

Indeks unik PostgreSQL membuatkan sisipan serentak bagi kunci yang sama menunggu dan kemudian menyelesaikannya kepada satu sisipan atau satu konflik. Hadkan masa menunggu itu. Jika transaksi pertama mengomit dengan cepat, permintaan kedua boleh membaca dan memainkan semula hasilnya. Jika penantian mencapai hadnya, kembalikan hasil idempotency_in_progress yang eksplisit dengan panduan percubaan semula. Jangan gunakan SELECT peringkat aplikasi sebelum membuat keputusan untuk menyisip, dan jangan sekali-kali menulis ganti cap jari atau hasil yang disimpan semasa konflik.

Invarian utama ialah apabila baris pesanan kelihatan, hasil keidempotennan dan peristiwa outbox juga kelihatan; apabila transaksi dibalikkan, tiada satu pun daripadanya yang kelihatan. Kes kegagalan menyusul secara langsung. Ranap proses sebelum penciptaan pesanan akan membalikkan transaksi, jadi percubaan semula boleh bersaing lagi. Respons yang hilang selepas pesanan dikomit akan dimainkan semula. Kunci yang sama dengan parameter berbeza tidak akan dilaksanakan. Daripada dua permintaan seiras yang tiba bersama, hanya satu yang boleh melepasi kekangan keunikan dan menyelesaikan penulisan. Penolakan domain deterministik seperti kupon yang telah tamat tempoh boleh disimpan sebagai respons ralat yang stabil di dalam transaksi. Kegagalan yang tidak dikomit seperti sambungan pangkalan data yang terputus tidak disimpan.

Jika penciptaan pesanan tidak boleh kekal dalam transaksi pendek, gunakan mesin keadaan dua fasa. Transaksi pendek pertama mengomit IN_PROGRESS, lease_expires_at, dan attempt_token yang meningkat secara monotonik. API mengembalikan 202, atau permintaan lain dengan kunci tersebut membaca titik akhir status. Selepas pajakan tamat tempoh, pekerja baharu menggunakan operasi banding-dan-tukar (compare-and-swap) untuk menuntut token yang lebih besar. Setiap kemas kini penyiapan mengesahkan token tersebut, yang menghalang pekerja lama daripada menulis ganti hasil yang lebih baharu selepas ia bersambung semula. Pesanan masih memerlukan kekangan keunikan pada ID operasi perniagaan, dan kesan sampingan masih menggunakan outbox atau kunci keidempotennan hiliran. Baris status dengan TTL tetapi tanpa token pengambilalihan membolehkan pekerja lama dan baharu berjalan bersama; ia hanya melengahkan perlumbaan penduaan.

Panggilan pembayaran, inventori, dan pemberitahuan luaran tidak tergolong di dalam transaksi pangkalan data tempatan. Pemilik menulis baris outbox OrderCreated bersama pesanan, dan pengguna (consumer) menghantarnya selepas komit. Pengguna menyahaslikan pendua mengikut ID peristiwa. Panggilan ke API pembayaran atau inventori menggunakan kunci stabil yang diperoleh daripada ID pesanan dan jenis operasi. Penghantaran mesej sekurang-kurangnya sekali (at-least-once) kemudiannya tidak menjadi pengecasan sekurang-kurangnya sekali. Jika perkhidmatan hiliran tidak mempunyai antara muka idempoten, gunakan mesin keadaan tempatan, pertanyaan penyelarasan, atau pampasan manual; outbox sahaja tidak menghapuskan pendua luaran.

Pengekalan mengikut kontrak percubaan semula. Pelaksanaan awam Stripe membenarkan pembuangan kunci selepas tempoh pengekalan. Ini menunjukkan bahawa kunci boleh mempunyai kitaran hayat, tetapi tempohnya tidak universal. Tetapkan expires_at untuk meliputi giliran luar talian mudah alih yang paling lama, percubaan semula get laluan, dan tetingkap main semula manual. Penggunaan semula kunci lama selepas pembersihan adalah operasi baharu. Jika satu checkout_id mesti menghasilkan paling banyak satu pesanan selama-lamanya, letakkan kekangan unik kekal pada orders supaya ia masih menyekat pendua selepas rekod keidempotennan tiada.

Redis boleh mencache hasil yang telah selesai, tetapi ia tidak seharusnya menjadi satu-satunya sempadan ketepatan. Selepas SET NX berjaya, proses boleh ranap sama ada sebelum atau selepas komit pesanan, dan TTL kunci boleh tamat sebelum permintaan yang perlahan selesai. Cache juga tidak boleh mengomit pesanan dan outbox secara atomik. Biarkan kekangan keunikan pangkalan data menentukan “dicipta paling banyak sekali”. Gunakan cache hanya untuk mengurangkan pembacaan bagi main semula yang kerap, dengan rekod pangkalan data sebagai sumber kebenaran tunggal (source of truth).

Ujian memerlukan lebih daripada dua permintaan berurutan. Lindungi sekurang-kurangnya kes-kes ini: 50 permintaan serentak dengan satu kunci menghasilkan satu pesanan dan satu peristiwa outbox; kunci yang sama dengan parameter berbeza mengembalikan konflik; pemutusan sambungan secara paksa selepas komit tetapi sebelum respons masih memainkan semula pesanan asal; kunci yang sama boleh mencuba semula selepas pembalikan transaksi; tingkah laku selepas tamat tempoh kunci sepadan dengan kontrak; dua penyewa yang menggunakan kunci mentah yang sama kekal diasingkan; dan penghantaran pendua kepada pengguna tidak menduplikasi caj. Pantau tuntutan baharu, main semula, konflik parameter, masa tamat penantian serentak, operasi belum selesai yang paling lama, pembalikan transaksi, kelengahan outbox, dan pembersihan kunci yang tamat tempoh. Log tidak boleh memasukkan badan permintaan sensitif yang dikaitkan dengan kunci.

Contoh Jawapan yang Kukuh

“Saya akan mentakrifkan satu kunci keidempotennan sebagai satu niat penciptaan bagi satu penyewa pada POST /orders. Klien menjananya pada panggilan pertama dan menyimpannya untuk percubaan semula masa tamat. Pelayan mengambil cap jari medan pesanan yang disahkan bersama versi API. Kunci dan cap jari yang sama boleh dimainkan semula; kunci yang sama dengan cap jari berbeza adalah konflik.

Dalam PostgreSQL, saya akan menjadikan (tenant_id, endpoint, idempotency_key) sebagai kunci utama. Selepas pengesahan permintaan, setiap panggilan bersaing untuk pemilikan dengan INSERT ... ON CONFLICT DO NOTHING. Pemenang mencipta pesanan, menulis baris outbox, dan menyimpan status pertama serta badan respons dalam satu transaksi pendek sebelum mengomit. Yang kalah tidak boleh mencipta pesanan lain. Ia menunggu transaksi pertama, membaca rekod, dan mengembalikan hasil asal untuk cap jari yang sepadan. Jika had menunggu dalaman dicapai, ia memberitahu pemanggil bahawa operasi masih sedang dijalankan. Ranap selepas komit tetapi sebelum respons HTTP dipulihkan sebagai pesanan yang sama semasa percubaan semula.

Saya tidak akan memanggil pembayaran atau inventori secara segerak di dalam transaksi tersebut. Pengguna outbox menghantar tindakan tersebut dan menyerahkan kunci yang diperoleh daripada ID pesanan kepada setiap operasi hiliran. Rekod keidempotennan wujud hanya untuk tetingkap percubaan semula maksimum, jadi jika sesi daftar keluar (checkout session) tidak boleh mencipta pesanan kedua sama sekali, checkout_id juga mendapat kekangan unik kekal pada pesanan.

Saya akan mengakhiri dengan suntikan kerosakan (fault injection) bagi keserentakan, pemutusan sambungan, dan mesej pendua. Saya akan mengesahkan kiraan pesanan, hasil keidempotennan, dan peristiwa outbox dan bukannya sekadar memeriksa HTTP 200. Dalam persekitaran pengeluaran, saya akan memantau kadar main semula, konflik parameter kunci yang sama, masa tamat penantian serentak, pembalikan transaksi, dan kelengahan outbox.”

Kesilapan Lazim

  • Semak baris, kemudian sisip pesanan → dua permintaan boleh melihat tiada baris dan kedua-duanya menulis → biarkan kekangan keunikan pangkalan data dan pengendalian konflik atomik memilih pemilik.
  • Hanya cari kunci keidempotennan → penyewa boleh bertembung atau menerima respons penyewa lain → hadkan skop mengikut penyewa dan titik akhir yang disahkan, dan lakukan pengesahan kebenaran semula semasa main semula.
  • Kembalikan hasil lama tanpa membandingkan parameter → penggunaan semula kunci secara tidak sengaja membuatkan pesanan lama kelihatan seperti pesanan baharu → simpan cap jari permintaan yang dinormalkan dan tolak ketidakpadanan.
  • Komit pesanan, kemudian tulis hasil keidempotennan → ranap di antara penulisan meninggalkan pesanan tanpa rekod penyahaslian penduaan → komit pesanan, hasil, dan outbox dalam satu transaksi pangkalan data.
  • Panggil perkhidmatan pembayaran di dalam transaksi pangkalan data → masa tamat rangkaian memanjangkan tempoh kunci, dan pembayaran yang berjaya tidak boleh dibalikkan secara atomik di peringkat tempatan → tulis entri outbox dalam transaksi dan gunakan keidempotennan hiliran di luarnya.
  • Hanya gunakan kunci Redis dengan TTL → selepas ranap atau tamat tempoh, ia tidak dapat mendedahkan sama ada pesanan telah dikomit atau memainkan semula hasilnya → gunakan kekangan pangkalan data untuk ketepatan dan cache hanya untuk pecutan.
  • Cache setiap ralat → permintaan yang diperbetulkan masih boleh menerima ralat pengesahan lama, manakala kegagalan fana menjadi mustahil untuk dicuba semula → jangan simpan kegagalan pengesahan sebelum pemilikan; bezakan hasil domain yang stabil daripada kegagalan yang tidak dikomit.
  • Janjikan penyahaslian penduaan kekal selepas memadam rekod permintaan → kunci yang telah tamat tempoh dianggap sebagai operasi baharu → terbitkan tetingkap pengekalan dan gunakan kunci unik domain untuk invarian kekal.

Soalan Susulan dan Jawapan

Susulan 1: Apakah yang harus diterima oleh permintaan kedua semasa permintaan pertama belum dikomit?

Dengan reka bentuk transaksi pendek, konflik indeks unik membuatkan sisipan kedua menunggu. Hadkan kedua-dua penantian pangkalan data dan pelaksanaan titik akhir di bawah masa tamat klien. Jika transaksi pertama mengomit tepat pada masanya, permintaan kedua membaca dan memainkannya semula. Pada had tersebut, kembalikan hasil idempotency_in_progress yang boleh dikenal pasti dan pemasaan percubaan semula dan bukannya mencipta pesanan. Tugas yang panjang menggunakan keadaan IN_PROGRESS yang dikomit dan titik akhir status supaya sambungan HTTP tidak terus diduduki.

Susulan 2: Apakah yang berlaku jika perkhidmatan ranap selepas komit tetapi sebelum menulis respons HTTP?

Itulah sebabnya hasilnya disimpan. Pesanan, peristiwa outbox, dan snapshot respons telah dikomit bersama-sama. Percubaan semula berkonflik pada kunci unik, mengesahkan cap jari, dan mengembalikan status serta badan yang disimpan. Nilai Boolean "diproses" semata-mata tidak akan memberitahu pelayan pesanan mana yang hendak dikembalikan, dan pemanggil tidak dapat membezakan kejayaan daripada hasil yang tidak diketahui.

Susulan 3: Bagaimana jika kunci keidempotennan yang sama membawa jumlah yang berbeza?

Kembalikan 409 idempotency_key_reused, rekod metrik konflik, dan jangan laksanakan penciptaan pesanan. Jangan sekali-kali menulis ganti rekod lama dengan parameter baharu atau mengembalikan pesanan lama secara senyap. Cap jari mesti merangkumi jumlah, mata wang, item, medan rujukan penghantaran, dan setiap medan lain yang mengubah hasil perniagaan, serta versi API atau kanonikalisasi.

Susulan 4: Pekerja yang berjalan lama terhenti seketika, pekerja lain mengambil alih pajakannya yang tamat tempoh, dan pekerja lama bersambung semula. Apakah yang menghalang penulisan lapuk?

Tingkatkan attempt_token pada setiap pengambilalihan. Perubahan keadaan pesanan, penulisan penyiapan, dan tugas hiliran yang boleh dikawal membawa token tersebut dan mengemas kini hanya apabila ia sepadan dengan nilai semasa. Penulisan pekerja lama gagal selepas ia bersambung semula, jadi ia membaca keadaan baharu dan keluar. Jika sistem hiliran tidak dapat mengesahkan token tersebut, ia masih memerlukan API idempoten yang berkunci mengikut ID operasi perniagaan yang stabil atau proses penyelarasan dan pampasan. Pajakan sahaja tidak boleh menarik balik kesan sampingan luaran yang telah dihantar.

Susulan 5: Mengapa tidak menggunakan kekangan unik pada orders.checkout_id sahaja?

Kekangan tersebut menghalang pesanan kedua dan merupakan perlindungan kekal yang sangat baik. Ia tidak mentakrifkan sama ada parameter berbeza di bawah satu kunci keidempotennan berkonflik, status apa yang dikembalikan oleh permintaan pertama, cara bertindak balas semasa kerja sedang berjalan serentak, atau perkara yang perlu dilakukan untuk pemanggil tanpa checkout_id. Rekod keidempotennan menyediakan protokol peringkat permintaan dan main semula hasil; kunci unik perniagaan melindungi invarian domain. Kedua-duanya beroperasi pada lapisan yang berbeza.

Sumber awam

Soalan berkaitan