GH GambleHub

Paginare și cursoare

1) De ce este necesară paginarea

Pagination limitează cantitatea de date transmise și redate de client, reduce sarcina pe stocare/rețele și stabilește o modalitate deterministă de a „merge” prin colecție. În sistemele reale, paginația nu este doar 'page = 1 & limit = 50', ci și un set de contracte de protocol și invarianți de consistență.

Obiective tipice:
  • Latența și controlul memoriei la cerere.
  • Navigare stabilă la schimbarea unui set de date (inserare/ștergere).
  • Capacitatea de a relua dintr-un loc (reluare).
  • Caching și preîncărcare (prefetch).
  • Protecție împotriva abuzului (limitarea ratei, presiunea excesivă).

2) Modele de paginare

2. 1 OFFSET/LIMIT (paginat)

Ideea: "sari peste N linii, întoarce M.
Pro: simplitate, compatibil cu aproape orice SQL/NoSQL.

Contra:
  • Degradare liniară: OFFSET-urile mari au ca rezultat o scanare completă/salt-cost.
  • Instabilitatea în timpul inserțiilor/ștergerilor între cereri (compensează „float”).
  • Este dificil să se asigure o „regenerabilitate” exactă.
Exemplu SQL:
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;

2. 2 Cursor/Keyset/Căutare-paginare

Ideea: "Continuă cu cheia K. "Cursorul este poziția din setul sortat.

Argumente pro:
  • O (1) acces pentru a continua cu index.
  • Stabilitate în timpul modificărilor de colectare.
  • Cea mai bună latență în „pagini” profunde.
Contra:
  • Avem nevoie de chei de sortare strict definite, unice și monotone.
  • Mai dificil de implementat și depanat.
Exemplu SQL (caută):
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 Continuarea jetoanelor

Ideea: serverul returnează un token opac în care „poziția” este codificată (și, eventual, starea cioburilor/filtrelor). Clientul nu înțelege internii și pur și simplu returnează un jeton pentru pagina următoare.
Pro: flexibilitate, capacitatea de a schimba schema fără a rupe API.
Contra: gestionarea duratei de viață a tokenului, compatibilitatea cu depozitele.

2. 4 Cursoare de timp și logică

Bazat pe timp: „toate înregistrările până la T”, cursor - timbru de timp (potrivit pentru fire numai de adăugare).
Log-secvență/offset-based: cursor - offset în jurnal (Kafka offset, jurnal seq).
ID-uri monotonice globale: Snowflake/UUIDv7 ca chei sortabile pentru căutare stabilă.

3) Proiectarea de cursuri și jetoane

3. 1 Proprietăți bune ale cursorului

Opac-Clientul este format independent.
Autor/integritate: semnătură HMAC pentru a preveni spoofing/manipulare.
Context: include sortare, filtre, versiune schema, chiriaș/ciob.
Durata de viață: TTL și „non-reluare” la schimbarea indexurilor/drepturilor de acces.
Dimensiune: compact (<= 1-2 KB) potrivit pentru URL.

3. 2 Format token

Stiva recomandată: compresie JSON (zstd/deflate) HMAC.

Structura sarcinii utile (exemplu):
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, sarcină utilă)' se adaugă în partea de sus și totul este codificat într-un token șir.

3. 3 Siguranţă

Sign (HMAC/SHA-256).
Opțional criptați (AES-GCM) în prezența valorilor sensibile (PII).
Validarea serverului: versiunea, TTL, autoritatea utilizatorului (RBAC/ABAC).

4) Coerență și invarianți

4. 1 Sortare stabilă

Utilizați determinismul complet: „ORDINE PRIN DESC, id DESC”.
Cheia de sortare trebuie să fie unică (adăugați 'id' ca tiebreaker).
Indexul trebuie să se potrivească cu indexul de acoperire.

4. 2 Instantanee și izolare

Pentru paginile non-jumbo, utilizaţi instantaneu consistent pentru citire (MVCC/txid).
Dacă instantaneul este impracticabil (scump/o mulțime de date), formulați un contract: "cursorul returnează elemente strict înainte de poziție. "Acest lucru este natural pentru fluxurile de știri.

4. 3 Inserții/ștergeri între pagini

Seek-model minimizează „duplicate/omisiuni”.
Comportamentul de ștergere/modificare a documentelor: rare „găuri” între pagini sunt permise, dar nu „înapoi în timp”.

5) Scheme de indexare și ID-uri

Indicii compuși sunt strict în ordine de sortare: „(created_at DESC, id DESC)”.
ID-uri monotone: Snowflake/UUIDv7 dau ordine în timp → a accelera căutarea.
Tastele fierbinți: distribuiți prin cheie de ciob (de exemplu, "chiriaș _ id'," regiune ") și sortați în interiorul ciobului.
Generatoare ID: evitați coliziunile și „înclinarea ceasului” - sincronizarea timpului, „regresia” în timpul salturilor NTP.

6) Cross-cioburi paginare

6. 1 Scheme

Scatter-Gather: cereri paralele pentru toate cioburile, cursuri de căutare locală, apoi fuzionarea k-way pe agregator.
Cursoare Per-Shard: Tokenul conține poziții pe fiecare ciob.
Limitat fan-out-limita numărul de cioburi pe pas (rata de limitare/timeout buget).

6. 2 jetoane pentru multi-shard

Store array '{shard _ id, last_pos}'. În pasul următor, reluați pentru fiecare ciob activ și țineți din nou, oferind pagina sortată la nivel global.

7) Contracte de protocol

7. 1 REST

Cerere:

GET /v1/orders? limit=50&cursor=eyJ2IjoiMyIsInNvcnQiOiJjcmVh... (opaque)
Răspuns:
json
{
"items": [ /... / ],
"page": {
"limit": 50,
"next_cursor": "eyJ2IjozLCJwb3MiOiJjcmVh...==",
"has_more": true
}
}
Recomandări:
  • „limit” cu o legătură superioară (de exemplu, max = 200).
  • 'next _ cursor' lipsește dacă 'has _ more = false'.
  • GET idempotence, cachability of responses without 'next _ cursor' (prima pagină cu filtre fixe și instantaneu).

7. 2 GraphQL (abordare releu)

Contract tipic de „conexiune”:
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
}

„cursor” trebuie să fie opac și semnat; nu utilizați „Base64 brut (id)” fără HMAC.

7. 3 gRPC

Utilizați '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 fire și WebSockets

Pentru benzi continue: cursor ca "ultimul văzut offset/ts'.

Suport 'CV _ from' în timpul reconectării:
json
{ "action":"subscribe", "topic":"orders", "resume_from":"2025-10-31T12:00:00Z#987654321" }

8) Caching, Preload, CDN

ETag/If-None-Match pentru prima pagină cu filtre stabile.
Cache-Control cu un scurt TTL (de exemplu, 5-30 s) pentru liste publice.
Prefetch: returnați 'next _ cursor' și sugestii ('Link: rel =' next' '), clientul poate preîncărca pagina următoare.
Variații: Luați în considerare „filtru/sort/locale/chiriaș” ca fiind cheia părții cache.

9) Gestionarea sarcinii și limitarea

Limita superioară, de ex. 200.
Backpressure pe partea serverului: dacă timpul de solicitare este> buget, reduceți „limita” din răspuns (și spuneți explicit clientului dimensiunea reală a paginii).
Limitele ratei per utilizator/token/chiriaș.
Timeout/Retry: pauză exponențială, cereri repetate idempotente.

10) Aspecte UX

Derulare împotriva numerotării: cursoare infinite de →; numărul de pagini → offset (dar explicați inexactitatea la actualizarea datelor).
Reveniți la butonul de plasare: Stocați stiva de cursor a clientului.
Pagini goale: Dacă 'has _ more = false', nu afișați butonul More.
Limite stabile: afișați exact "total" numai dacă este ieftin (altfel este o abordare aproximativă _ total ").

11) Cazuri de testare și margine

Liste de verificare:
  • Sortare stabilă: Elementele cu același "ts' nu" clipesc ".
  • Inserturi/Ștergeri - Duplicatele nu apar la intersecția paginii.
  • Schimbați filtrele între pagini: Token trebuie respins ca depășit/incompatibil.
  • Token TTL: Eroare validă după expirare.
  • Adâncime mare: Latența nu crește liniar.
  • Multishard: fuziune corectă, absența cioburilor „lente” de înfometare.
Exemplu de testare bazat pe proprietate (pseudo code):
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch

12) Observabilitate și SLO

Măsurători:
  • 'list _ request _ latency _ ms' (P50/P95/P99) după lungimea paginii.
  • 'search _ index _ hit _ ratio' (proporția cererilor lăsate de indexul de acoperire).
  • 'next _ cursor _ invalid _ rate' (erori de validare/TTL/semnătură).
  • 'merge _ fanout' (numărul de cioburi implicate pe pagină).
  • 'duplicates _ on _ boundary' and 'gaps _ on _ boundary' (detectarea pe telemetria clientului).
Busteni/vectorizare:
  • Corelați "cursor _ id' în jurnalele, sarcina utilă a măștii.
  • Tag se întinde: 'page _ size', 'source _ shards',' db _ index _ used '.
Exemplu SLO:
  • Disponibilitate: 99. 9% pe metodele „Listă”.
  • Latență: P95 <200 ms pentru 'page _ size <= 50' într-o taxă locală.
  • Eroare token: <0. 1% din numărul total de apeluri.

13) Migrații și interoperabilitate

Activați „v” în token și sprijiniți versiunile mai vechi ale săptămânilor N.
Când schimbați tastele de sortare - trimiteți o eroare „moale” „409 Conflict” cu un prompt pentru a efectua o listă proaspătă fără un cursor.
Caz catastrofal (răgetul tuturor jetoanelor): schimbați 'signing _ key _ id' și respingeți-le pe cele vechi.

14) Exemple de implementare

14. 1 Generare token (pseudocod)

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

14. 2 Validare token

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 Căutați interogare cu cheie compozită

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) Siguranță și conformitate

Nu includeți câmpuri brute în jetoane din care pot fi derivate PII.
Semnează și limitează TTL.
Încercați să faceți jetoanele intolerabile între utilizatori (scrieți „sub/chiriaș/roluri” în sarcină utilă și verificați în timpul validării).
Log doar hash-uri token.

16) Erori frecvente și anti-modele

Base64 (id) ca un cursor: ușor de falsificat/ridicat, rupe contractul atunci când se schimbă genul.
Fără întrerupător de cravată: "COMANDĂ PRIN DESC" fără "id' → duplicate/salturi.
Schimbați filtrele între pagini fără a invalida jetonul.
Adânc OFFSET: lent și imprevizibil.
Jetoane fără versiune și TTL.

17) Implementarea listei de verificare mini

1. Definiți sortarea și adăugați un întrerupător unic de cravată.
2. Creați un index de acoperire pentru această ordine.
3. Selectați modelul: căutați + token opac.
4. Implementați semnarea token (și, dacă este necesar, criptarea).
5. Stabiliți TTL și versioning.
6. Formulați și documentați contractele 'has _ more', 'next _ cursor'.
7. Luați în considerare o schemă cross-shard (dacă este necesar) și k-way fuziona.
8. Adăugați valori, alerte și SLO-uri.
9. Acoperiți frontierele paginii bazate pe proprietate cu teste.
10. Descrieți strategia de migrare pentru token-uri.

18) Scurte recomandări pentru alegerea unei abordări

Directoare/căutări în care „numărul paginii” și totalul aproximativ sunt importante: să spunem „OFFSET/LIMIT” + cache; raportează că totalul este aproximativ.
Feed-uri, analize, liste profunde, RPS ridicat: cursor/caută numai.
Colecții shardy/distribuite: cursoare per-shard + jeton de îmbinare.
Threads/CDC: cursoare ca compensează/ts cu CV-ul.

19) Exemplu de acord API (rezumat)

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

Răspunsul include întotdeauna "pagina. limita „,” pagină. has_more', optional 'page. next_cursor'.
Cursorul este opac, semnat, cu TTL.
Sortarea este deterministă 'ORDER by created_at DESC, id DESC'.
Setați schimbarea comportamentului-Items nu „du-te înapoi” în raport cu cursorul.
Metricile și erorile sunt standardizate: 'invalid _ cursor', 'expired _ cursor', 'mismatch _ filters'.

Acest articol oferă principii arhitecturale și modele gata făcute pentru a proiecta paginări care rămân rapide, previzibile și sigure chiar și în datele mari, shardy și schimbarea activă a recordurilor.

Contact

Contactați-ne

Scrieți-ne pentru orice întrebare sau solicitare de suport.Suntem mereu gata să ajutăm!

Pornește integrarea

Email-ul este obligatoriu. Telegram sau WhatsApp sunt opționale.

Numele dumneavoastră opțional
Email opțional
Subiect opțional
Mesaj opțional
Telegram opțional
@
Dacă indicați Telegram — vă vom răspunde și acolo, pe lângă Email.
WhatsApp opțional
Format: cod de țară și număr (de exemplu, +40XXXXXXXXX).

Apăsând butonul, sunteți de acord cu prelucrarea datelor dumneavoastră.