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.
- 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ă.
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.
- Avem nevoie de chei de sortare strict definite, unice și monotone.
- Mai dificil de implementat și depanat.
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.
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).
- Corelați "cursor _ id' în jurnalele, sarcina utilă a măștii.
- Tag se întinde: 'page _ size', 'source _ shards',' db _ index _ used '.
- 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.