Problem
Mobil tətbiq müştərini debet etmək üçün POST /payments göndərir. Server ödənişi icra edib bazaya yazır, amma telefon Wi-Fi-dan mobil internetə keçdiyi üçün cavab itir. Klient timeout görür və ağlabatan addımı atır: təkrar göndərir. Qoruma olmasa, müştəridən iki dəfə pul çıxır.
Klient ilk sorğunun emaldan əvvəl, emal zamanı və ya sonra uğursuz olduğunu bilə bilməz. Timeout, retry, mesajın təkrar çatdırılması və ya düyməyə iki dəfə basan istifadəçi olan hər sistemdə eyni sorğu bəzən bir dəfədən çox çatacaq. Bir əməliyyatı bir neçə dəfə icra etmək onu bir dəfə icra etməklə eyni nəticə verirsə, o idempotent-dir. İdempotentlik dublikatların gəlməsinin qarşısını almır; onları zərərsiz edir.
Təbii idempotentlik
Bəzi əməliyyatlar təbiətinə görə idempotentdir:
- Tam dəyərlə
PUT /users/7/email: təkrar eyni dəyəri təyin edir. DELETE /orders/15: ilk çağırışdan sonra sifariş yoxdur; sonrakılar heç nəyi dəyişmir (cavab kodu fərqli ola bilər, vəziyyət yox).- Biznes açarı üzərində
INSERT ... ON CONFLICT DO NOTHING. - "Statusu irəlilət" əvəzinə "statusu SHIPPED et".
Digərləri belə deyil: POST /payments, "balansa 10 əlavə et", "email göndər". Mümkün olduqda əməliyyatı təbii idempotent olacaq şəkildə yenidən dizayn edin, məsələn yaradılan obyektin identifikatorunu klientin göndərməsinə icazə verməklə. Bu mümkün deyilsə, idempotency key istifadə edin.
Idempotency key-lər
Klient hər məntiqi əməliyyat üçün unikal açar (UUID kifayətdir) yaradır və onu Idempotency-Key kimi header-də göndərir. Həmin əməliyyatın hər təkrarında eyni açar, yeni əməliyyat üçün yeni açar istifadə olunur. Server açarı nəticə ilə birlikdə saxlayır və təkrar gəldikdə yenidən icra etmək əvəzinə saxlanmış nəticəni qaytarır.
Klient Server Baza
| POST + key K1 | |
|-------------------->| insert K1 (IN_PROGRESS) |
| |------------------------->|
| | ödəniş, nəticəni yaz |
| (cavab itdi) |------------------------->|
| POST + key K1 | |
|-------------------->| K1 tapıldı: COMPLETED |
|<--------------------| saxlanmış cavab qaytarılır|Cədvəl
CREATE TABLE idempotency_record ( key text PRIMARY KEY, request_hash text NOT NULL, status text NOT NULL, -- IN_PROGRESS, COMPLETED response_code int, response_body jsonb, created_at timestamptz NOT NULL DEFAULT now());Əsl qoruyucu PRIMARY KEY-dir. Eyni açarlı iki paralel sorğu insert üzərində yarışır və baza yalnız birinin qalib gəlməsinə icazə verir:
try { repo.insertInProgress(key, hash); // varsa unique pozuntusu} catch (DuplicateKeyException e) { var existing = repo.find(key); if (!existing.requestHash().equals(hash)) { throw new IdempotencyKeyReuseException(); // 422 } if (existing.isCompleted()) return existing.toResponse(); throw new RequestInProgressException(); // 409, klient sonra təkrarlayır}var response = paymentService.charge(request);repo.complete(key, response);return response;Üç detala diqqət edin.
- Request hash. Sorğu gövdəsinin hash-ini saxlayın. Eyni açar fərqli məzmunla gəlirsə, bu retry yox, klient xətasıdır; əlaqəsiz cavab qaytarmaq əvəzinə rədd edin.
- Atomarlıq. Biznes dəyişikliyi və
COMPLETEDqeydi eyni bazada olduqda eyni transaction-da commit edilməlidir. Onda crash qeydsiz ödəniş və ya ödənişsiz qeyd buraxa bilməz. - IN_PROGRESS idarəsi. Proses
IN_PROGRESSinsert etdikdən sonra, tamamlamazdan əvvəl çökərsə, sonrakı retry-lar həmişəlik bloklanar. Ya bütün işi tək transaction edin (insert crash ilə geri qayıdır), ya da müddətdən sonraIN_PROGRESSqeydinin təhvil alınmasına icazə verən timeout əlavə edin.
Xarici yan təsirlər
Əməliyyat ödəniş provayderini çağırırsa, tək bazadakı transaction onu əhatə edə bilməz. Provayder dəstəkləyirsə, öz sabit identifikatorunuzu ona idempotency key kimi ötürün ki, çağırışın təkrarı onların tərəfində də dublikat sayılsın. Əvvəl niyyəti qeyd edin, provayderi çağırın, sonra nəticəni yazın; bərpa zamanı yenidən debet etmək əvəzinə provayderdən həmin identifikatorun statusunu soruşun.
Müddət bitməsi
Açarları sonsuz saxlamaq olmaz. Ən uzun real klient retry pəncərəsindən uzun saxlama müddəti seçin, sənədləşdirin və köhnə sətirləri planlı job ilə silin. Müddət bitdikdən sonra qayıdan açar yeni sayılır, buna görə klientlər açarları fərqli əməliyyatlar üçün təkrar istifadə etməməlidir. created_at üzrə partial indeks və ya cədvəl partitioning-i böyük cədvəllərdə təmizləməni ucuz saxlayır.
Mesaj consumer-ləri
Eyni ideya mesaj handler-lərinə də aiddir. At-least-once çatdırılma consumer-in eyni mesajı iki dəfə görə biləcəyi deməkdir. Emal olunmuş mesaj id-lərini saxlayın (və ya effektə təbii unique constraint qoyun) və dublikatları keçin, ideal olaraq effektlə eyni transaction-da.
Tipik səhvlər
SELECTilə "açar var?" yoxlayıb sonra insert etmək. İki sorğu yoxlamadan birlikdə keçir. Unique constraint istifadə edin.- Klient kitabxanasında hər retry-da yeni açar yaratmaq; bu bütün mexanizmi sıradan çıxarır.
- Yalnız uğurları keşləmək. İlk cəhd biznes xətası ilə bitibsə, retry-ın həmin xətanı təkrar qaytarmalı, yoxsa yenidən işləməli olduğunu qərara alın.
- Dublikat zamanı ilkin cavab əvəzinə boş gövdəli
200qaytarmaq. - Artıq təhlükəsiz olan
GET-ə idempotency key tətbiq etmək.
Güzəştlər
Idempotency saxlanması hər sorğuya bir yazı və təmizləmə job-u əlavə edir. Saxlanmış cavabları təkrar qaytarmaq cavab gövdələrini saxlamaq deməkdir, onlarda həssas data ola bilər; yalnız retry üçün lazım olanı saxlayın və datanın özü ilə eyni saxlama qaydalarını tətbiq edin. Aşağı dəyərli əməliyyatlar üçün təbii idempotentlik açar cədvəlindən ucuzdur.
HTTP kontraktı
İdempotency key-i API sənədində açıq təsvir edin ki, klientlər nə gözləyəcəklərini bilsinlər. Açarın formatı (məsələn UUID), maksimum uzunluğu, hansı endpoint-lərdə tələb olunduğu, saxlama müddəti və xəta halları yazılmalıdır. Tipik cavablar: eyni açar və eyni gövdə üçün ilkin cavabın təkrarı; eyni açar, fərqli gövdə üçün 422; emal hələ bitməyibsə 409 ilə yanaşı Retry-After.
Klient tərəfdə qayda sadədir: açarı istifadəçinin niyyəti yarandıqda yaradın (məsələn, "Ödə" düyməsi göstərildikdə) və onu yaddaşda saxlayın. Şəbəkə xətasında eyni açarla təkrarlayın. İstifadəçi səbəti dəyişib yenidən ödəyirsə, bu, yeni niyyətdir və yeni açar tələb edir.
Konkret ssenari: iki düymə basışı
İstifadəçi "Ödə" düyməsinə iki dəfə sürətlə basır. Klient hər basışda eyni niyyət üçün eyni açarı göndərir. İki sorğu eyni anda serverə çatır; ikisi də insert etməyə çalışır, baza yalnız birinə icazə verir. Qalib ödənişi icra edir, məğlub DuplicateKeyException alır, qeydi oxuyur və ya 409 qaytarır, ya da qalibin bitməsindən sonra saxlanmış cavabı verir. İstifadəçi bir ödəniş və bir cavab görür. Eyni mexanizm şəbəkə retry-ları və mesaj təkrar çatdırılmaları üçün də işləyir.
Son olaraq, idempotentliyi test edin. Eyni sorğunu eyni açarla iki dəfə və paralel şəkildə göndərən test yazın və yalnız bir yan təsirin yarandığını təsdiqləyin. Bu test mexanizmin yarış şəraitində də işlədiyini göstərir və gələcək dəyişikliklər zamanı onu qoruyur.
Yoxlama siyahısı
- Klientlərin təkrarladığı hər qeyri-idempotent endpoint idempotency key qəbul edir.
- Yarışı əvvəlki oxu yox, unique constraint həll edir.
- Request hash müqayisə olunur.
- Effekt və qeyd birlikdə commit olunur.
- Saxlama müddəti müəyyən edilib və köhnə qeydlər silinir.
Növbəti addım
Bütün bunların əsasında paralellik zəmanətləri durur; isolation səviyyələri və lost update məqaləsinə qayıdın. Keşlənmiş cavablar və idempotency qeydləri müddət bitməsi baxımından oxşardır, bax: caching strategiyaları. Transaction sərhədlərinin detalları üçün Spring transaction tələləri məqaləsini oxuyun.