페이지 네이션 및 커서
1) 페이지 매김이 필요한 이유
Pagination은 클라이언트가 전송하고 렌더링하는 데이터의 양을 제한하고 스토리지/네트워크의로드를 줄이며 컬렉션을 "보행" 하는 결정 론적 방법을 설정합니다. 실제 시스템에서 페이지 매김은 '페이지 = 1 & 한계 = 50' 일뿐만 아니라 일련의 프로토콜 계약 및 일관성 불변입니다.
일반적인 목표:- 요청 당 대기 시간 및 메모리 제어.
- 데이터 세트를 변경할 때 안정적인 탐색 (삽입/삭제).
- 장소에서 재개 할 수있는 능력 (재개).
- 캐싱 및 프리로드 (프리 페치).
- 남용으로부터 보호 (속도 제한, 역압).
2) Pagination 모델
2. 1 OFFSET/LIMIT (페이지)
아이디어: "N 라인을 건너 뛰고 돌아 오십시오".
장점: 거의 모든 SQL/NoSQL과 호환되는 단순성.
- 선형 저하: 큰 OFFSET은 전체 스캔/건너 뛰기 비용을 초래합니다.
- 요청 사이의 삽입/삭제 중 불안정성 (오프셋 "플로트").
- 정확한 "갱신 가능성" 을 보장하기는 어렵습니다.
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;
2. 커서/키셋/탐색 페이지화 2 개
아이디어: "K 키를 사용하십시오. "커서는 정렬 된 세트의 위치입니다.
장점:- O (1) 색인을 계속 사용할 수 있습니다.
- 수집 중 안정성.
- 깊은 "페이지" 에서 최고의 대기 시간.
- 엄격하게 정의되고 독특하며 단조로운 정렬 키가 필요합니다.
- 구현하고 디버깅하기가 더 어렵습니다.
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 연속 토큰
아이디어: 서버는 "위치" 가 인코딩 된 (및 파편/필터의 상태) 불투명 한 토큰을 반환합니다. 클라이언트는 내부를 이해하지 못하고 단순히 다음 페이지의 토큰을 반환합니다.
장점: 유연성, API를 깨지 않고 체계를 변경하는 기능.
단점: 토큰 수명 관리, 예금과의 호환성.
2. 4 시간 및 논리 커서
시간 기반: "모두 T까지 기록", 커서 - 타임 스탬프 (추가 전용 스레드에 적합).
로그 시퀀스/오프셋 기반: 커서-로그에서 오프셋 (Kafka offset, journal seq).
글로벌 단조로운 ID: 안정적인 탐색을위한 정렬 가능한 키로 Snowflake/UIDv7.
3) 코스 및 토큰 디자인
3. 좋은 커서 속성 1 개
Opaque-클라이언트는 형식에 독립적입니다.
저자/무결성: 스푸핑/조작을 방지하기위한 HMAC 서명.
상황: 정렬, 필터, 스키마 버전, 테넌트/샤드가 포함됩니다.
수명: 색인/액세스 권한을 변경할 때 TTL 및 "비 재생"
크기: DM에 적합한 컴팩트 (<= 1-2 KB).
3. 2 토큰 형식
권장 스택: JSON → 압축 (zstd/deplate) → Base64IM → HMAC.
페이로드 구조 (예):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 (비밀, 페이로드)' 가 상단에 추가되고 모든 것이 하나의 문자열 토큰으로 인코딩됩니다.
3. 3 안전
서명 (HMAC/Ś-256).
민감한 값 (PII) 이있는 경우 선택적으로 암호화 (AES-GCM).
서버 검증: 버전, TTL, 사용자 권한 (RBAC/ABAC).
4) 일관성과 불변
4. 1 안정적인 정렬
완전한 결정론을 사용하십시오: 'DESC, id DESC에 의한 주문'.
정렬 키는 고유해야합니다 ('id' 를 순위 결정자로 추가).
인덱스는 커버링 인덱스와 일치해야합니다
4. 스냅 샷 2 개 및 격리
점보가 아닌 페이지의 경우 읽기 일관된 스냅 샷 (MVCC/txid) 을 사용하십시오.
스냅 샷이 실용적이지 않으면 (비싼/많은 데이터) 계약을 체결하십시오. "커서는 위치 이전에 요소를 엄격하게 반환합니다. "이것은 뉴스 피드에 당연합니다.
4. 페이지 간 삽입/삭제 3 개
탐색 모델은 "복제/생략" 을 최소화합니다.
문서 삭제/수정 동작: 페이지 사이에 드문 "구멍" 이 허용되지만 "다시 시간" 은 허용되지 않습니다.
5) 인덱싱 및 ID 체계
복합 지수는 엄격하게 정렬 순서입니다: '(Pritten _ at DESC, id DESC)'.
모노톤 ID: Snowflake/UUIDv7은 시간 → 속도 향상 추구를 순서대로 제공합니다.
핫 키: 샤드 키 (예: '테넌트 _ id', '지역') 로 배포하고 파편 내부를 정렬하십시오.
ID 생성기: NTP 점프 중 충돌 및 "클럭 왜곡" - 시간 동기화, "회귀" 를 피하십시오.
6) 파편 간 페이지
6. 1 계획
Scatter-Gather: 모든 파편에 대한 병렬 요청, 현지 요청 과정, k-way는 애그리 게이터에서 병합됩니다.
파편 커서: 토큰에는 각 파편의 위치가 포함되어 있습니다.
바운드 팬 아웃 제한 단계당 파편 수 (속도 제한/타임 아웃 예산).
6. 멀티 샤드 용 토큰 2 개
저장 배열 '{shard _ id, last _ pos}'. 다음 단계에서 각 활성 파편에 대해 재개하고 다시 유지하여 전 세계적으로 정렬 된 페이지를 제공하십시오.
7) 의정서 계약
7. 1 REST
요청:
GET /v1/orders? limit=50&cursor=eyJ2IjoiMyIsInNvcnQiOiJjcmVh... (opaque)
답변:
json
{
"items": [ /... / ],
"page": {
"limit": 50,
"next_cursor": "eyJ2IjozLCJwb3MiOiJjcmVh...==",
"has_more": true
}
}
권장 사항:
- 상한으로 '제한' (예: max = 200).
- (PHP 3 = 3.0.6, PHP 4)
- (PHP 3 = 3.0.6, PHP 4)
7. 2 GraphQL (릴레이 방식)
일반적인 '연결' 계약: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
}
'커서' 는 불투명하고 서명되어야합니다. HMAC없이 "원시 Base64 (id)" 를 사용하지 마십시오.
7. 3 gRPC
(PHP 3 = 3.0.6, PHP 4)
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 개
연속 테이프의 경우: 커서는 "마지막으로 본 오프셋/ts" 입니다.
다시 연결하는 동안 '이력서' 를 지원합니다:json
{ "action":"subscribe", "topic":"orders", "resume_from":"2025-10-31T12:00:00Z#987654321" }
8) 캐싱, 프리로드, CDNName
안정적인 필터가있는 첫 번째 페이지의 ETag/If-None-Match
공개 목록에 대해 짧은 TTL (예: 5-30 초) 의 캐시 제어.
프리 페치: '다음 _ 커서' 를 반환하고 힌트 ('Link: rel = "nether"') 를 반환하면 클라이언트가 다음 페이지를 미리로드 할 수 있습니다.
변형: 캐시 부분의 키로 '필터/정렬/로케일/테넌트' 를 고려하십시오.
9) 로드 관리 및 제한
상한선 '한계' (예: 200.
서버 측 압력: 요청 시간이> 예산 인 경우 응답에서 '제한' 을 줄이십시오 (클라이언트에 실제 페이지 크기를 명시 적으로 알려줍니다).
사용자/토큰/테넌트 당 요율 제한.
시간 초과/재시도: 지수 일시 정지, 반복 된 요청.
10) UX 측면
번호 매기기 방지: 무한 스크롤 → 커서; 숫자 페이지 → 오프셋 (데이터를 업데이트 할 때 부정확성을 설명하십시오).
장소로 돌아갑니다: 클라이언트 커서 스택을 저장합니다.
빈 페이지: '더 많은 _ more = 잘못된' 경우 더 많은 버튼을 표시하지 마십시오.
안정적인 경계: 정확한 '총' 을 저렴한 경우에만 표시하십시오 (그렇지 않으면 대략적인 '접근 _ 총').
11) 테스트 및 엣지 케이스
체크리스트:- 안정적인 정렬: 동일한 'ts' 를 가진 항목은 "깜박이지" 않습니다.
- 삽입/삭제 - 복수는 페이지 교차점에 나타나지 않습니다.
- 페이지 간 필터 변경: 토큰은 더 이상 사용되지 않거나 호환되지 않는 것으로 거부되어야합니다.
- 토큰 TTL: 만료 후 유효한 오류.
- 깊이: 대기 시간이 선형으로 자라지 않습니다.
- 멀티 샤드: 올바른 병합 순서, 기아 "느린" 파편 없음.
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch
12) 관찰 및 SLO
메트릭:- '리스트 _ 요청 _ latency _ ms' (P50/P95/P99) 페이지 길이.
- (PHP 3 = 3.0.6, PHP 4)
- (PHP 3 = 3.0.6, PHP 4)
- 'merge _ fanout' (페이지 당 관련 파편 수).
- (PHP 3 = 3.0.6, PHP 4)
- 로그에서 '커서 _ id' 와 관련이 있습니다. 마스크 페이로드.
- 태그 범위: 'page _ size', 'source _ shards', 'db _ indx _ used'.
- 가용성: 99. '목록' 방법에서 9%.
- 대기 시간: 로컬 요금으로 '페이지 _ 크기 <= 50' 에 대한 P95 <200 ms.
- 토큰 오류: <0. 전체 통화 수의 1%.
13) 이주 및 상호 운용성
토큰에서 'v' 를 사용하고 이전 버전의 N 주를 지원합니다.
정렬 키를 변경할 때-커서없이 새로운 목록을 수행하라는 프롬프트와 함께 "소프트" 오류 '409 충돌' 을 보냅니다.
치명적인 경우 (모든 토큰의 포효): 'signing _ key _ id' 를 변경하고 오래된 경우를 거부하십시오.
14) 구현 예
14. 1 토큰 생성 (의사 코드)
python payload = json. dumps({...}). encode()
compressed = zlib. compress(payload)
mac = hmac_sha256(signing_key, compressed)
token = base64url_encode(mac + compressed)
14. 2 토큰 검증
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 복합 키가있는 쿼리를 찾으십시오
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) 안전 및 준수
PII를 도출 할 수있는 토큰에는 원필드를 포함하지 마십시오.
TTL에 서명하고 제한하십시오.
사용자간에 토큰을 견딜 수 없게 만드십시오 (페이로드에서 '하위/테넌트/역할' 을 작성하고 검증 중에 확인).
토큰 해시 만 기록하십시오.
16) 빈번한 오류 및 패턴 방지
커서로서의 Base64 (id): 가짜/픽업이 쉬우 며 종류를 변경할 때 계약을 위반합니다.
타이 브레이커 없음: 'id' → 중복/점프없이 'ORDER Łts DESC'.
토큰을 무효화하지 않고 페이지 간 필터를 변경하십시오.
깊은 관리: 느리고 예측할 수 없습니다.
버전이없는 토큰 및 TTL.
17) 미니 체크리스트 구현
1. 정렬을 정의하고 고유 한 타이 브레이커를 추가하십시오.
2. 이 순서에 대한 스패닝 인덱스를 만듭니다.
3. 모델을 선택하십시오: + 불투명 한 토큰을 찾으십시
4. 토큰 서명 (및 필요한 경우 암호화) 을 구현합니다.
5. TTL을 내려 놓고 버전을 정하십시오.
6. 포뮬레이션 및 문서 'has _ more', 'Next _ curder' 계약.
7. 크로스 샤드 체계 (필요한 경우) 및 k- 웨이 병합을 고려하십시오.
8. 메트릭, 알림 및 SLO를 추가하십시오.
9. 속성 기반 페이지 테두리를 테스트로 덮으십시오.
10. 토큰에 대한 마이그레이션 전략을 설명하십
18) 접근 방식 선택을위한 간단한 권장 사항
"페이지 번호" 와 대략적인 총계가 중요한 디렉토리/검색: 'OFFSET/LIMIT' + 캐시를 말합시다. 총계가 대략적이라고보고하십시오.
피드, 분석, 딥 리스트, 높은 RPS: 커서/찾기 만.
샤디/분산 컬렉션: 파편 당 커서 + 병합 토큰.
스레드/CDC: 이력서가있는 오프셋/t로 커서.
19) API 계약의 예 (요약)
'GET/v1/항목? 한계 = 50 & 커서 =... '
대답에는 항상 '페이지가 포함됩니다. ',' 페이지를 제한합니다. (PHP 3 = 3.0.6, PHP 4) 다음 _ 커서 '.
커서는 TTL로 불투명하고 부호가 있습니다.
정렬은 결정론적입니다. 'DESC, id DESC를 생성하십시오'.
변경 동작 항목을 설정하십시오-항목은 커서에 대해 "돌아 가지" 않습니다.
(PHP 3 = 3.0.6, PHP 4)
이 기사는 빅 데이터, 선명하고 적극적으로 변화하는 레코드 세트에서도 빠르고 예측 가능하며 보안을 유지하는 페이지 화를 설계하기위한 아키텍처 원칙과 기성품 패턴을 제공합니다.