GH GambleHub

Paginacja i kursory

1) Dlaczego potrzebna jest paginacja

Pagination ogranicza ilość danych przesyłanych i renderowanych przez klienta, zmniejsza obciążenie magazynu/sieci oraz określa deterministyczny sposób na „przejście” przez kolekcję. W systemach rzeczywistych paginacja to nie tylko 'strona = 1 & limit = 50', ale zbiór umów protokołowych i ciągłości.

Typowe cele:
  • Opóźnienie i kontrola pamięci na żądanie.
  • Stabilna nawigacja podczas zmiany zbioru danych (wstaw/usuń).
  • Możliwość wznowienia z miejsca (wznowienie).
  • Buforowanie i wstępne obciążenie (prefetch).
  • Ochrona przed nadużyciami (ograniczenie szybkości, ciśnienie wsteczne).

2) Modele paginacji

2. 1 OFFSET/LIMIT (paged)

Pomysł: „pominąć linie N, zwrócić M.”

Plusy: prostota, kompatybilna z prawie każdym SQL/NoSQL.

Minusy:
  • Liniowa degradacja: duże OFFSETs skutkują pełnym skanowaniem/pominięciem kosztów.
  • Niestabilność podczas wstawiania/usuwania między żądaniami (offsets „float”).
  • Trudno jest zapewnić dokładną „odnawialność”.
Przykład SQL:
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;

2. 2 Kursor/Keyset/Poszukiwanie paginacji

Pomysł: "zajmij się kluczem K. "Kursor jest pozycją w sortowanym zestawie.

Plusy:
  • O (1) dostęp do kontynuacji z indeksem.
  • Stabilność podczas zmian kolekcji.
  • Najlepsze opóźnienia w głębokich „stronach”.
Minusy:
  • Potrzebujemy ściśle określonych, unikalnych i monotonnych klawiszy.
  • Trudniejsze do wdrożenia i debugowania.
Przykład SQL (szukaj):
sql
-- Resumption after steam (created_at, id) = (:last_ts,:last_id)
SELECT
FROM orders
WHERE (created_at, id) < (:last_ts,:last_id)
ORDER BY created_at DESC, id DESC
LIMIT 50;

2. 3 Żetony kontynuacyjne

Idea: serwer zwraca nieprzezroczysty token, w którym „pozycja” jest kodowana (i ewentualnie stan odłamków/filtrów). Klient nie rozumie internali i po prostu zwraca token dla następnej strony.
Plusy: elastyczność, możliwość zmiany systemu bez łamania API.
Minusy: zarządzanie życiodajne, kompatybilność z depozytami.

2. 4 Kursory czasu i logiki

Oparte na czasie: „wszystkie rekordy do T”, kursor - znacznik czasu (nadaje się do gwintów tylko do aplikacji).
Log-sequence/offset-based: cursor - offset in the log (Kafka offset, journal seq).
Globalne identyfikatory monotoniczne: Snowflake/UUIDv7 jako sortowalne klucze do stabilnego poszukiwania.

3) Projektowanie kursów i żetonów

3. 1 Dobre właściwości kursora

Opaque-Klient jest niezależny w formacie.
Autorstwo/integralność: podpis HMAC zapobiegający spoofing/manipulacji.
Kontekst: obejmuje sortowanie, filtry, wersję schematu, najemcę/odłamek.
Czas życia: TTL i „non-replay” podczas zmiany indeksów/praw dostępu.
Rozmiar: kompaktowy (<= 1-2 KB) odpowiedni dla adresu URL.

3. 2 Format tokenu

Zalecany stosu: JSON → kompresja (zstd/deflate) → Base64URL → HMAC.

Struktura ładunku (przykład):
json
{
"v": 3 ,//token version
"sort": ["created_at:desc","id:desc"],
"pos": {"created_at":"2025-10-30T12:34:56. 789Z","id":"987654321"},
"filters": {"user_id":"42","status":["paid","shipped"]},
"tenant": "eu-west-1",
"shards": [{"s ": "a, "" hi":"..."}] ,//optional: cross-shard context
"issued_at": "2025-10-31T14:00:00Z",
"ttl_sec": 3600
}

'mac = HMAC (secret, payload)' jest dodawany na górze i wszystko jest kodowane w jednym symbolu.

3. 3 Bezpieczeństwo

Znak (HMAC/SHA-256).
Opcjonalnie szyfrować (AES-GCM) w obecności wartości wrażliwych (PII).
Walidacja serwera: wersja, TTL, autorytet użytkownika (RBAC/ABAC).

4) Spójność i niezmienne

4. 1 Stabilne sortowanie

Użyj pełnego determinizmu: 'ORDER BY ts DESC, id DESC'.
Klucz sortowania musi być unikalny (dodaj 'id' jako tiebreaker).
Indeks musi odpowiadać indeksowi pokrycia.

4. 2 Migawki i izolacja

W przypadku stron non-jumbo należy użyć spójnego migawki (MVCC/txid).
Jeśli migawka jest niewykonalna (drogie/dużo danych), sformułuj umowę: "kursor zwraca elementy ściśle przed pozycją. "To naturalne dla wiadomości.

4. 3 Wpisy/skreślenia między stronami

Model Search minimalizuje „duplikaty/pominięcia”.
Zachowanie usuwania/modyfikacji dokumentów: dozwolone są rzadkie „dziury” między stronami, ale nie „wstecz w czasie”.

5) Systemy indeksacji i identyfikacji

Indeksy kompozytowe są ściśle w kolejności sortowej: '(created_at DESC, id DESC)'.
Identyfikatory monotonowe: Snowflake/UUIDv7 dać kolejność w czasie → przyspieszenie poszukiwania.
Klucze gorące: rozprowadzać za pomocą shard-key (na przykład 'lokator _ id',' region ') i sortować wewnątrz odłamka.
Generatory ID: unikać kolizji i „skew zegara” - synchronizacji czasu, „regresji” podczas skoków NTP.

6) Paginacja krzyżowa

6. 1 Programy

Scatter-Gather: równoległe wnioski do wszystkich odłamków, lokalne kursy poszukiwania, a następnie k-way połączyć na agregatorze.
Kursory Per-Shard: Token zawiera pozycje na każdym odłamku.
Ograniczony wentylator-out-Limit liczba odłamków na krok (ograniczenie tempa/budżet timeout).

6. 2 Żetony do multi-shard

Przechowuj tablicę '{shard _ id, last_pos}'. W następnym kroku, wznów dla każdego aktywnego odłamka i przytrzymaj ponownie, dając globalnie sortowaną stronę.

7) Umowy protokołowe

7. 1 ODPOCZYNEK

Żądanie:

GET /v1/orders? limit=50&cursor=eyJ2IjoiMyIsInNvcnQiOiJjcmVh... (opaque)
Odpowiedź:
json
{
"items": [ /... / ],
"page": {
"limit": 50,
"next_cursor": "eyJ2IjozLCJwb3MiOiJjcmVh...==",
"has_more": true
}
}
Zalecenia:
  • „limit” z górnej granicy (na przykład max = 200).
  • brak 'next _ cursor', jeśli 'has _ more = false'.
  • GET idempotence, cachability of responses without 'next _ cursor' (pierwsza strona ze stałymi filtrami i migawkami).

7. 2 GraphQL (Podejście przekaźnikowe)

Typowa umowa „połączenia”:
graphql type Query {
orders(first: Int, after: String, filter: OrderFilter): OrderConnection!
}

type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
}

type OrderEdge {
node: Order!
cursor: String! // opaque
}

type PageInfo {
hasNextPage: Boolean!
endCursor: String
}

„żłobek” musi być nieprzezroczysty i podpisany; nie używać „raw Base64 (id)” bez HMAC.

7. 3 gRPC

Użyj 'page _ size' i 'page _ token':
proto message ListOrdersRequest {
string filter = 1;
int32 page_size = 2;
string page_token = 3; // opaque
}

message ListOrdersResponse {
repeated Order items = 1;
string next_page_token = 2; // opaque bool has_more = 3;
}

7. 4 wątki i gniazda internetowe

Dla taśm ciągłych: kursor jako "last seen offset/ts'.

Wsparcie 'wznowić _ z' podczas ponownego połączenia:
json
{ "action":"subscribe", "topic":"orders", "resume_from":"2025-10-31T12:00:00Z#987654321" }

8) Buforowanie, ładowanie wstępne, CDN

ETag/If-None-Match dla pierwszej strony ze stabilnymi filtrami.
Cache-Control z krótkim TTL (na przykład 5-30 s) dla list publicznych.
Prefetch: zwraca 'next _ cursor' i wskazówki ('Link: rel =' next ''), klient może wstępnie załadować następną stronę.
Warianty: za klucz części pamięci podręcznej należy uznać 'filtr/sort/locale/najemca'.

9) Zarządzanie obciążeniem i ograniczenie

Górna granica 'limit', np. 200.
Backpressure po stronie serwera: jeśli czas żądania jest> budżet, zmniejszyć „limit” w odpowiedzi (i wyraźnie powiedzieć klientowi rzeczywisty rozmiar strony).
Limity stawki na użytkownika/token/najemcę.
Timeout/Retry: pauza wykładnicza, powtarzające się żądania idempotentne.

10) Aspekty UX

Przewijanie z numeracją: nieskończone przewijanie → kursory; liczba stron → przesunięcie (ale wyjaśnić niedokładność podczas aktualizacji danych).
Powrót do przycisku miejsca: Przechowywać stos kursora klienta.
Puste strony: Jeśli 'has _ more = false', nie pokazuj przycisku More.
Stabilne granice: pokazać dokładne 'całkowite' tylko wtedy, gdy jest tanie (w przeciwnym razie jest to przybliżone 'podejście _ total').

11) Badania i skrzynie krawędziowe

Listy kontrolne:
  • Stabilne sortowanie: Przedmioty z tym samym 'ts' nie „mrugają”.
  • Wstawki/Usuwa - Duplikaty nie pojawiają się na przecięciu strony.
  • Zmień filtry między stronami: Token musi zostać odrzucony jako przestarzały/niezgodny.
  • Token TTL: Poprawny błąd po wygaśnięciu.
  • Wielka głębokość: Opóźnienie nie rośnie liniowo.
  • Multishard: prawidłowe połączenie, brak głodu „powolne” odłamki.
Przykład badania opartego na właściwościach (kod pseudo):
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch

12) Obserwowalność i SLO

Metryka:
  • 'list _ request _ latency _ ms' (P50/P95/P99) według długości strony.
  • „search _ index _ hit _ ratio” (odsetek wniosków pozostawionych przez indeks pokrycia).
  • 'next _ cursor _ invalid _ rate' (błędy dotyczące walidacji/TTL/podpisu).
  • „merge _ fanout” (liczba zaangażowanych odłamków na stronę).
  • 'duplikaty _ on _ boundary' i 'gaps _ on _ boundary' (wykrywanie na telemetrii klienta).
Dzienniki/śledzenie:
  • Koreluj 'cursor _ id' w dziennikach, ładunek maski.
  • Tag spans: 'page _ size', 'source _ shards', 'db _ index _ used'.
Przykład SLO:
  • Dostępność: 99. 9% w metodach „Lista”.
  • Opóźnienie: P95 <200 ms dla 'page _ size <= 50' w lokalnej opłacie.
  • Błąd tokenu: <0. 1% całkowitej liczby połączeń.

13) Migracja i interoperacyjność

Włącz 'v' w tokenie i obsługuj starsze wersje N weeks.
Przy zmianie klawiszy sortowania - wyślij „miękki” błąd '409 Conflict' z szybkością do wykonania świeżej aukcji bez kursora.
Katastroficzny przypadek (ryk wszystkich żetonów): zmień 'signing _ key _ id' i odrzuć stare.

14) Przykłady wdrażania

14. 1 Generowanie tokenów (pseudokoda)

python payload = json. dumps({...}). encode()
compressed = zlib. compress(payload)
mac = hmac_sha256(signing_key, compressed)
token = base64url_encode(mac + compressed)

14. 2 Walidacja tokenu

python raw = base64url_decode(token)
mac, compressed = raw[:32], raw[32:]
assert mac == hmac_sha256(signing_key, compressed)
payload = json. loads(zlib. decompress(compressed))
assert now() - payload["issued_at"] < payload["ttl_sec"]
assert payload["filters"] == req. filters

14. 3 Szukaj zapytania z kluczem kompozytowym

sql
-- Page # 1
SELECT FROM feed
WHERE tenant_id =:t
ORDER BY ts DESC, id DESC
LIMIT:limit;

-- Next pages, continued after (ts0, id0)
SELECT FROM feed
WHERE tenant_id =:t
AND (ts <:ts0 OR (ts =:ts0 AND id <:id0))
ORDER BY ts DESC, id DESC
LIMIT:limit;

15) Bezpieczeństwo i zgodność

Nie włączać pól surowych do żetonów, z których można wyprowadzić PII.
Podpisz i ograniczaj TTL.
Spróbuj sprawić, aby żetony nie były tolerowane między użytkownikami (zapisz 'sub/najemca/role' w ładunku użytkownika i sprawdź podczas walidacji).
Zaloguj tylko hashes tokenów.

16) Częste błędy i anty-wzory

Base64 (id) jako kursor: łatwy do podrobienia/odebrania, zerwanie umowy przy zmianie rodzaju.
No tie-breaker: 'ORDER BY ts DESC' bez 'id' → duplikaty/skoki.
Zmień filtry między stronami bez unieważniania tokena.
GŁĘBOKIE PRZESUNIĘCIE: Powolne i nieprzewidywalne.
Żetony bez wersji i TTL.

17) Wdrożenie mini listy kontrolnej

1. Zdefiniuj rodzaj i dodaj unikalny wyłącznik.
2. Utwórz indeks spanning dla tego zamówienia.
3. Wybierz model: szukaj + nieprzezroczysty token.
4. Zaimplementuj podpisywanie tokenów (oraz, w razie potrzeby, szyfrowanie).
5. Połóż TTL i wersioning.
6. Formuła i dokument 'has _ more', kontrakty 'next _ cursor'.
7. Rozważyć schemat cross-shard (w razie potrzeby) i połączenie k-way.
8. Dodaj mierniki, wpisy i SLO.
9. Pokryć granice strony opartej na nieruchomości testami.
10. Opisz strategię migracji dla żetonów.

18) Krótkie zalecenia dotyczące wyboru podejścia

Katalogi/wyszukiwania, gdzie „numer strony” i przybliżona suma są ważne: powiedzmy 'OFFSET/LIMIT' + cache; zgłosić, że suma jest przybliżona.
Kanały, analityka, listy głębokie, wysoki RPS: kursor/szukać tylko.
Kolekcje Shardy/distributed: per-shard cursors + token fuzji.
Wątki/CDC: kursory jako offsety/ts z CV.

19) Przykład umowy API (streszczenie)

'GET/v1/items? limit = 50 & cursor =... '

Odpowiedź zawsze zawiera 'strona. strona limit ','. has_more', opcjonalnie 'page. next_cursor'.
Kursor jest nieprzezroczysty, podpisany, z TTL.
Sort jest deterministyczny 'ORDER BY created_at DESC, id DESC'.
Ustaw zmiany zachowania-Elementy nie „wracać” w stosunku do kursora.
Mierniki i błędy są standaryzowane: 'invalid _ cursor', 'expired _ cursor', 'mismatch _ filters'.

Ten artykuł zapewnia zasady architektoniczne i gotowe wzory do projektowania paginacji, która pozostaje szybka, przewidywalna i bezpieczna nawet w dużych danych, shardy i aktywnie zmieniających się zestawów nagrań.

Contact

Skontaktuj się z nami

Napisz do nas w każdej sprawie — pytania, wsparcie, konsultacje.Zawsze jesteśmy gotowi pomóc!

Rozpocznij integrację

Email jest wymagany. Telegram lub WhatsApp są opcjonalne.

Twoje imię opcjonalne
Email opcjonalne
Temat opcjonalne
Wiadomość opcjonalne
Telegram opcjonalne
@
Jeśli podasz Telegram — odpowiemy także tam, oprócz emaila.
WhatsApp opcjonalne
Format: kod kraju i numer (np. +48XXXXXXXXX).

Klikając przycisk, wyrażasz zgodę na przetwarzanie swoich danych.