如何基于“seq”令牌实现可恢复的分页会话?

解读

国内面试官问“seq 令牌可恢复分页”时,核心想验证两点

  1. 你是否真正用过 CouchDB 的 changes feed,还是只背了概念;
  2. 你是否能在 网络闪断、客户端重启、微服务扩缩容 等真实场景下,保证“断点续传、不丢不重、顺序一致”。
    回答必须围绕 seq 的本质(数据库全局有序变更号) 展开,给出 可落地的 HTTP 参数组合与容错策略,而不是泛泛而谈“用 last_seq 就行”。

知识点

  1. seq(update_seq):数据库级单调递增的变更序号,每条 changes 记录都带一个 seq 值,全局有序、可比较
  2. changes feed 三种模式:normal、longpoll、continuous;可恢复分页只能依赖 normal 模式,因为能回卷。
  3. 关键参数
    • since=seq:从指定序号后开始,包含该序号之后的第一条变更
    • limit=N:每页条数,与 seq 配合实现分页
    • include_docs=false:减少带宽,移动端尤其重要
  4. 可恢复会话三要素
    • 持久化 seq:客户端本地写文件、写 SQLite、写 Redis 均可,必须落盘
    • 幂等写入:业务层用 _id 做幂等键,防止重复消费
    • 异常分类
      – 网络 502/504:直接重试同一 seq;
      – 4xx:立即中断并报警,不盲目重试
      – CouchDB 返回 last_seq ≤ since:说明已追到最新,主动休眠
  5. 国内云厂商差异
    • 阿里云 CouchDB 7 集群默认开 max_changes_limit=10000单次 limit 过大直接 413,需拆页。
    • 腾讯云容器场景下,Pod 重启后 IP 变,不能把 seq 放内存,必须外挂云盘或对象存储。

答案

实现步骤(可直接背给面试官):

  1. 首次拉取
    GET /db/_changes?since=0&limit=500&include_docs=false
    返回 "last_seq":"1234"持久化到本地文件 seq.token

  2. 后续分页
    读取本地 seq.token 得到 1234,再发
    GET /db/_changes?since=1234&limit=500&include_docs=false
    拿到新一批变更,更新 seq.token 为本次返回的 last_seq

  3. 断点续传
    进程重启或网络闪断后,直接读 seq.token,无需回溯;
    若 CouchDB 返回 results:[]last_seq == since,说明无新数据,线程休眠 30 s 再探

  4. 并发与顺序
    单线程顺序消费,seq 严格递增,保证因果顺序;
    如需多线程,按分片字段(如 userId)做队列拆分,每条线程维护独立 seq.token,避免全局锁。

  5. 完整容错代码骨架(伪码)

    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);  
    }
    

拓展思考

  1. seq 与 _rev 的区别:seq 是 数据库级全局序号,_rev 是 文档级 MVCC 版本号;前者用于追变更,后者用于冲突解决,不可混用
  2. 超大库全量同步优化
    • 先调用 /db/_changes?since=0&limit=1 拿到当前 last_seq估算总页数
    • 多段并发 拉取,每段 10 w 条,段内顺序、段间无序,最后合并 seq.token,适合国内凌晨低峰期做数据仓库导数
  3. 与 PouchDB 离线场景结合
    移动端 PouchDB 通过 db.changes({since: localSeq, live: false, limit: 200}) 复用同一套 seq 机制,实现弱网离线、恢复后秒级追平,面试可举 政务微信小程序离线填报 案例,突出业务价值。