分割和游标
1)为什么需要分区
分割限制了客户端传输和渲染的数据量,减轻了存储/网络的负载,并指定了确定性"走遍"集合的方式。在现实世界中,分割不仅是"page=1&limit=50",而且是一组协议合同和一致性不变量。
示范目标:- 控制请求的潜伏期和内存。
- 更改数据集(插入/删除)时保持导航稳定。
- 从现场恢复的能力。
- 腰带和预装(预装)。
- 防止滥用(rate限制,backpressure)。
2)分割模型
2.1 OFFSET/LIMIT(页面)
想法:"跳过N行,返回M"。
优点:简单,几乎与任何SQL/NoSQL兼容。
- 线性降解:大OFFSET 导致完全扫描/skip-cost。
- 在查询之间插入/删除时不稳定性(偏移"浮动")。
- 很难提供准确的"可再生性"。
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;
2.2 Cursor/Keyset/Seek分离
想法:"继续使用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 Continuation tokens(不透明令牌)
想法:服务器返回一个opaque令牌,其中"位置"(以及可能的shards/过滤器状态)被编码。客户无法理解内脏,只需返回下一页的令牌即可。
优点:灵活性,能够在不打破API的情况下更改电路。
缺点: 代币寿命管理,代币兼容性.
2.4时间和逻辑游标
基于时间:"所有条目均为T",光标为时间标签(仅适用于上下文流)。
Log-sequence/offset:光标-log中的偏移(Kafka offset, journal seq)。
Global monotonic IDs: Snowflake/UUIDv7作为稳定搜索的可分类密钥。
3)课程和令牌设计
3.1好光标属性
不透明度(opaque):客户端与格式无关。
作者/完整性:HMAC签名以防止篡改/操纵。
上下文:包括排序,过滤器,模式版本,tenant/shard。
使用寿命:更改索引/访问权限时的TTL和"无效"(非重复)。
尺寸:紧凑型(<=1-2 KB),适合URL。
3.2令牌格式
推荐堆栈:JSON →压缩(zstd/deflate)→ Base64URL → 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(秘密,payload)",并且全部编码为单个字符串令牌。
3.3安全性
签字(HMAC/SHA-256)。
在存在敏感值(PII)时可选地加密(AES-GCM)。
服务器验证:版本、TTL、用户权限(RBAC/ABAC)。
4)一致性和不变性
4.1稳定排序
使用完整的确定性:"ORDER BY ts DESC, id DESC"。
排序键必须是唯一的(将"id"添加为tiebreaker)。
索引必须与排序(覆盖索引)匹配。
4.2快照(snapshot)和隔离
对于"不稳定"页面,请使用read-consistent snapshot (MVCC/txid)。
如果snapshot不切实际(昂贵/大量数据),则制定合同:"光标返回项目,严格先于位置。"新闻录像带很自然。
4.3页面之间的插入/删除
Seek模型最小化"重复/跳过"。
记录删除/更改时的行为:允许页面之间出现罕见的"漏洞",但不允许时间倒退。
5)索引和ID模式
复合索引严格按排序顺序排列:"(created_at DESC,id DESC)"。
单调ID:Snowflake/UUIDv7给出时间顺序→加速seek。
热键:分布在shard-key(例如'tenant_id'、'region'等)上,然后在shard-key内排序。
ID生成器:避免冲突和"未来时钟"(clock skew)-时间同步,NTP跳跃中的"回归"。
6)Cross Shard Pagination
6.1个方桉
Scatter-Gather:对所有沙丁鱼的并行查询,本地seek课程,然后是聚合器上的k-way merge。
Per-Shard Cursors:令牌包含每个令牌的位置。
受限制的禁区:一步限制硬币的数量(等级限制/定时预算)。
6.2多重硬币代币
存储数组'{shard_id, last_pos}'。在接下来的步骤中,为每个活动的shard复活,然后再次闪烁,给出一个全球排序的页面。
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)。
- 如果"has_more=false",则不存在"next_cursor"。
- GET的幂等性,非"next_cursor"响应的可缓存(固定过滤器和快照的头版)。
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
}
"cursor"必须不透明且签名;不使用HMAC的"原始Base64 (id)"。
7.3 gRPC
使用"page_size"和"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线程和WebSockets
对于连续磁带:游标为"最后看到的offset/ts"。
重构时支持"resume_from":json
{ "action":"subscribe", "topic":"orders", "resume_from":"2025-10-31T12:00:00Z#987654321" }
8) Keshing,预装,CDN
ETag/If-None-Match用于具有稳定过滤器的首页。
具有短TTL的Cache-Control(例如5-30 s)用于公共列表。
Prefetch:返回"next_cursor"和提示("Link: rel="next"),客户端可以预览下一页。
变体:将"过滤器/sort/locale/tenant"作为腰果的钥匙。
9)负载管理和限制
"极限"的上限,例如200。
Server side backpressure:如果查询时间>预算,请减少响应中的"限制"(并明确告知客户端实际页面大小)。
每个用户/令牌/tenant的价格限制。
Timeout/Retry:指数暂停、偶数重复查询。
10) UX方面
滚动与编号:无限滚动→光标;数字页面→ offset(但在更新数据时解释不准确)。
"返回到位"按钮:存储客户端游标堆栈。
空白页面:如果"has_more=false",请勿显示"更多"按钮。
稳定的界限:仅在价格便宜时才显示准确的"总"(否则为近似的"approx_total")。
11)测试和边缘桉例
支票单:- 稳定排序:具有相同'ts'的元素不会"闪烁"。
- 插入/删除:页面交界处不会出现重复。
- 更改页面之间的过滤器:令牌必须被拒绝为"过时/不兼容"。
- 令牌TTL:到期时正确错误。
- 深度:潜伏期没有线性增长。
- Multichard:正确的merge顺序,没有"慢速"的硬币。
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch
12)可观察性和SLO
度量标准:- "list_request_latency_ms" (P50/P95/P99)按页面长度排列。
- "seek_index_hit_ratio"(在覆盖索引上离开的查询比例)。
- "next_cursor_invalid_rate" (验证错误/TTL/签名)。
- "merge_fanout"(每页涉及的shards数量)。
- "duplicates_on_boundary"和"gaps_on_boundary"(客户端遥测中的细节)。
- 在日志中关联"cursor_id",掩盖付费。
- 标记"page_size"、"source_shards"、"db_index_used"。
- 可用性:99。9%的"列表"方法。
- 潜伏期:P95 <200 ms for 'page_size<=50'在本地魅力下。
- 令牌错误:<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个带有复合密钥的Seek查询
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。
尝试使令牌在用户之间不耐受(在付费下载中刻上"sub/tenant/roles",并在验证时进行钻探)。
仅编写令牌哈希。
16)频繁的错误和反模式
Base64 (id)作为光标:易于伪造/选择,在更改排序时打破合同。
缺席决胜局:"ORDER BY ts DESC"没有"id" →重复/跳跃。
在页面之间更改过滤器而不会破坏令牌。
Deep OFFSET:缓慢而不可预测。
没有版本和TTL的令牌。
17)实施迷你支票
1.定义排序并添加唯一的tie-breaker。
2.按照此顺序创建覆盖索引。
3.选择模型:seek+不透明令牌。
4.实现令牌签名(并在必要时加密)。
5.放置TTL和转换。
6.建立和记录合同"has_more"、"next_cursor"。
7.想想一个交叉沙丁鱼方案(如果需要)和k-way merge。
8.添加度量标准、Alerta和SLO。
9.覆盖基于属性的页面边界测试。
10.描述令牌的迁移策略。
18)关于选择方法的简要建议
"页码"和近似总数很重要的目录/搜索:假设"OFFSET/LIMIT"+缓存;报告总数是近似值。
磁带,分析师,深层列表,高RPS:仅cursor/seek。
硬质/分布式集合:按硬质cursors+merge令牌。
流/CDC:游标为offset/ts并恢复。
19) API约定示例(摘要)
`GET /v1/items?limit=50&cursor=...`
答桉总是包括'page。limit`, `page.has_more',可选的"页面"。next_cursor`.
光标是不透明的,签名,带有TTL。
排序是确定性的:"ORDER BY created_at DESC,id DESC"。
更改集时的行为:项目不相对于游标"返回"。
度量和错误是标准化的:"invalid_cursor","expired_cursor","mismatch_filters"。
本文给出了结构原理和现成的图案来设计分区,即使在大数据,硬化和积极变化的记录集环境中,分区仍保持快速,可预测和安全。