Projekt systemu walidacji biletów QR: od trybu offline do idempotentności
Kontroler skanuje kod QR biletu, system sprawdza go w bazie i oznacza jako użyty. Główny wymóg: jeden bilet — jedno wejście. Proste zadanie na pierwszy rzut oka komplikują awarie sieci, kolejki i wielu kontrolerów. Prześledźmy ewolucję rozwiązań od walidacji offline do niezawodnego przetwarzania serwerowego.
Tryb offline wykorzystuje podpis cyfrowy (ECC lub RSA). QR zawiera dane biletu i podpis, kontroler weryfikuje kluczem publicznym bez sieci. Problem: brak synchronizacji między kontrolerami. Zrzut ekranu biletu przechodzi przez każdą bramkę niezależnie.
Stan serwera i aktualizacje atomowe
Jedno repozytorium rozwiązuje problem synchronizacji. Każde skanowanie to zapytanie do serwera z atomową aktualizacją statusu.
Przykład w PostgreSQL:
UPDATE tickets
SET status = 'wasted', used_at = NOW()
WHERE id = :ticket_id AND status = 'active'
RETURNING *;
Jeśli zwrócony zostanie wiersz — bilet został wykorzystany, wejście dozwolone. Brak wiersza — bilet już użyty lub nieistniejący.
Klasyczny problem: utrata odpowiedzi z serwera. Kontroler w strefie słabego sygnału wysyła zapytanie, serwer aktualizuje bazę, ale odpowiedź nie dociera. Limit czasu, ponowne skanowanie — serwer odmawia, klient nie wchodzi mimo opłaconego biletu.
Idempotentność na poziomie API
Rozwiązanie — zapytania idempotentne z unikalnym kluczem (UUID), generowanym przy otwarciu sesji skanowania. Klucz rejestruje próbę, umożliwia ponowienia bez duplikacji.
Logika w transakcji (read committed):
begin ;
-- 1. Rejestrujemy próbę
INSERT INTO processed_requests (ticket_id, idempotency_key)
VALUES (:ticket_id, :idempotency_key)
ON CONFLICT (ticket_id, idempotency_key)
DO NOTHING RETURNING *;
-- 2. Jeśli rows_affected == 0: rollback, zwróć SUCCESS
-- 3. Wykorzystujemy bilet
UPDATE tickets
SET status = 'wasted', used_at = NOW()
WHERE id = :ticket_id AND status = 'active'
RETURNING *;
-- 4. Jeśli rows_affected == 0: rollback, zwróć ERROR
commit;
-- 5. Zwróć SUCCESS
Transakcja zapewnia atomowość: albo klucz + status zaktualizowane, albo nic. Unikalny indeks na (ticket_id, idempotency_key) zapobiega wyścigom — drugie zapytanie czeka lub przechodzi do DO NOTHING.
Ponowienie w tej samej sesji zwraca sukces, nawet jeśli pierwsza odpowiedź zginęła.
Head-of-Line Blocking i utrata kontekstu
Rzeczywisty problem: kontroler pod presją kolejki zamyka sesję bez odpowiedzi. Nowe skanowanie generuje świeży idempotency_key. Serwer widzi użyty bilet z obcym kluczem — odmowa.
Kontroler nie odróżni prawowitego klienta od oszusta ze zrzutem ekranu. HoL-blokowanie wymaga ostatecznej odpowiedzi przed kolejnym biletem.
Kompromis: zwracać timestamp ostatniego użycia. Kontroler ocenia: "2 minuty temu — ta sama osoba". Ryzyko: grupa ze wspólnym QR przechodzi kolejno.
Kluczowe wyzwania:
- Limity czasu sieci bez utraty stanu
- Synchronizacja wielu kontrolerów
- Czynnik ludzki w kolejkach
- Równowaga niezawodności i UX
Co jest ważne
- Atomowy UPDATE z warunkiem status='active' zapobiega podwójnemu wykorzystaniu
- Idempotentność przez UUID + unikalny indeks gwarantuje ponowienia bez duplikatów
- Transakcje (read committed) zapewniają sekwencyjną widoczność zmian
- HoL-blokowanie nieuniknione dla 100% niezawodności bez biometrii
- Kompromis timestamp przenosi decyzję na operatora
Perspektywy ulepszeń
Statyczne QR się dezaktualizują. Dynamiczne tokeny lub challenge-response z jednorazowymi OTP rozwiązują problem. Kontroler żąda nonce od serwera, klient podpisuje — serwer weryfikuje i oznacza atomowo.
Biometria (face ID) lub NFC eliminują zrzuty ekranu, ale wymagają sprzętu. QUIC minimalizuje utratę pakietów, ale nie rozwiązuje utraty kontekstu.
Skalowanie: Redis do cache idempotency_keys, sharding po ticket_id. Monitorowanie: metryki failed_scans po timestamp, A/B-testy kompromisów.
— Editorial Team
Brak komentarzy.