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ść”.
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”.
- Potrzebujemy ściśle określonych, unikalnych i monotonnych klawiszy.
- Trudniejsze do wdrożenia i debugowania.
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.
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).
- Koreluj 'cursor _ id' w dziennikach, ładunek maski.
- Tag spans: 'page _ size', 'source _ shards', 'db _ index _ used'.
- 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ń.