如何基于“seq”令牌实现可恢复的分页会话?
解读
国内面试官问“seq 令牌可恢复分页”时,核心想验证两点:
- 你是否真正用过 CouchDB 的 changes feed,还是只背了概念;
- 你是否能在 网络闪断、客户端重启、微服务扩缩容 等真实场景下,保证“断点续传、不丢不重、顺序一致”。
回答必须围绕 seq 的本质(数据库全局有序变更号) 展开,给出 可落地的 HTTP 参数组合与容错策略,而不是泛泛而谈“用 last_seq 就行”。
知识点
- seq(update_seq):数据库级单调递增的变更序号,每条 changes 记录都带一个 seq 值,全局有序、可比较。
- changes feed 三种模式:normal、longpoll、continuous;可恢复分页只能依赖 normal 模式,因为能回卷。
- 关键参数:
since=seq:从指定序号后开始,包含该序号之后的第一条变更。limit=N:每页条数,与 seq 配合实现分页。include_docs=false:减少带宽,移动端尤其重要。
- 可恢复会话三要素:
- 持久化 seq:客户端本地写文件、写 SQLite、写 Redis 均可,必须落盘。
- 幂等写入:业务层用
_id做幂等键,防止重复消费。 - 异常分类:
– 网络 502/504:直接重试同一 seq;
– 4xx:立即中断并报警,不盲目重试;
– CouchDB 返回last_seq ≤ since:说明已追到最新,主动休眠。
- 国内云厂商差异:
- 阿里云 CouchDB 7 集群默认开
max_changes_limit=10000,单次 limit 过大直接 413,需拆页。 - 腾讯云容器场景下,Pod 重启后 IP 变,不能把 seq 放内存,必须外挂云盘或对象存储。
- 阿里云 CouchDB 7 集群默认开
答案
实现步骤(可直接背给面试官):
-
首次拉取
GET/db/_changes?since=0&limit=500&include_docs=false
返回"last_seq":"1234",持久化到本地文件 seq.token。 -
后续分页
读取本地 seq.token 得到1234,再发
GET/db/_changes?since=1234&limit=500&include_docs=false
拿到新一批变更,更新 seq.token 为本次返回的 last_seq。 -
断点续传
进程重启或网络闪断后,直接读 seq.token,无需回溯;
若 CouchDB 返回results:[]且last_seq == since,说明无新数据,线程休眠 30 s 再探。 -
并发与顺序
单线程顺序消费,seq 严格递增,保证因果顺序;
如需多线程,按分片字段(如 userId)做队列拆分,每条线程维护独立 seq.token,避免全局锁。 -
完整容错代码骨架(伪码)
String since = readSeqToken(); while (true) { JsonObject resp = httpGet(host + "/db/_changes?since=" + since + "&limit=500"); for (JsonElement row : resp.getAsJsonArray("results")) { processDoc(row.getAsJsonObject().get("id").getAsString()); // 幂等写入 } since = resp.get("last_seq").getAsString(); writeSeqToken(since); // **落盘** if (resp.getAsJsonArray("results").size() < 500) sleep(30_000); }
拓展思考
- seq 与 _rev 的区别:seq 是 数据库级全局序号,_rev 是 文档级 MVCC 版本号;前者用于追变更,后者用于冲突解决,不可混用。
- 超大库全量同步优化:
- 先调用
/db/_changes?since=0&limit=1拿到当前last_seq,估算总页数; - 用 多段并发 拉取,每段 10 w 条,段内顺序、段间无序,最后合并 seq.token,适合国内凌晨低峰期做数据仓库导数。
- 先调用
- 与 PouchDB 离线场景结合:
移动端 PouchDB 通过db.changes({since: localSeq, live: false, limit: 200})复用同一套 seq 机制,实现弱网离线、恢复后秒级追平,面试可举 政务微信小程序离线填报 案例,突出业务价值。