如何存储用户行为序列并保证顺序?

解读

国内业务场景里,用户行为序列(点击、曝光、支付、分享等)既是实时推荐系统的“燃料”,也是风控、运营回溯的核心证据。面试官问“怎么存、怎么保序”,想确认三件事:

  1. 你是否理解 CouchDB 的最终一致性多主复制模型,知道它在“顺序”这件事上的天然短板;
  2. 能否在“文档数据库”范式内,用_id、_rev、附件、本地序列等原生机制,设计出既高可用又低成本的可落地方案;
  3. 是否具备“离线优先→移动断网→增量同步→云端归并”的完整闭环思维,这是国内 IoT、小程序、骑手端 App 选 CouchDB 的根本原因。
    一句话:不是“能不能”,而是“怎么权衡一致性、写入吞吐、移动端流量费用”。

知识点

  • _id 自增技巧:CouchDB 的 _id 全局字典序排序,可把“用户维度 + 毫秒时间戳 + 随机尾缀”拼成可排序字符串,实现“单用户行为保序”而无需额外索引。
  • _local 文档:以 _local 开头的文档不参与复制,可用来存放“客户端本地自增序号”,解决断网时离线写顺序问题。
  • update_handler:服务器端原子函数,可在写入时把“上一版本最后一条序列号”自动加 1,实现云端集中式序号,避免客户端伪造。
  • 附件(attachment):行为里常带图片、日志文件,直接挂附件走二进制流,省掉转 base64 的 30% 流量,国内 4G/5G 场景省费明显。
  • 多主冲突:同一用户在不同边缘节点同时写,CouchDB 会生成冲突分支,需用conflict=true 参数定期合并,按“业务时间戳优先”策略保留一条,其余转冷存。
  • ** Mango 索引**:对“用户+日期”建partial_index,把历史冷数据排除,保证热查询 200 ms 内返回,符合国内“90 天热点”合规要求。
  • 复制过滤:用selector 过滤只同步必要字段,避免把 50 KB 大附件拉回移动端,节省用户流量,面试时提到“工信部 2021 年个人信息保护测评”会加分。

答案

给出一套可直接落地的“三阶段”方案,兼顾离线、低流量、可审计。

阶段 1:客户端离线写入

  1. 本地维护一个 _local/seq 文档,保存“已同步序号”和“待同步队列”。
  2. 新行为产生时,先写进 IndexedDB(PouchDB),_id 格式:
    behavior::<userId>::<ts>::<nanoId>
    其中 ts 取当前毫秒,nanoId 用 6 位随机串防碰撞,保证单用户内严格递增
  3. 队列满 100 条或网络恢复时,批量 PUT,一次请求压缩成 gzip,节省 70% 流量,符合国内运营商套餐敏感场景。

阶段 2:云端集中序号

  1. 设计 update_handler 函数 addSeq,接收行为 JSON,原子读取“用户最新序号”并 +1,返回带 globalSeq 的新文档。
  2. 同一用户并发写时,CouchDB 自动产生冲突分支;后台定时任务用 /_find?conflicts=true 抓取冲突,按业务时间戳保留最新,其余写入冷存库(按年分区),满足网络安全法 6 个月日志留存要求。
  3. 对外查询提供两层索引:
    • Mango 索引 {"userId":1, "globalSeq":1},用于“单用户顺序翻页”;
    • 视图 _design/behavior_count 按天 reduce,给运营大屏秒级 PV/UV,避免全表扫描。

阶段 3:双向同步与审计

  1. 移动端用 PouchDB 的 live+retry 模式,长连接断开后 5 秒自动重试,弱网场景下用户体验平滑。
  2. 服务端开启 require_valid_user,关闭 _users 匿名写,行为文档里带 deviceIdHash 字段,满足等保 2.0 个人信息脱敏要求。
  3. 每日凌晨用 /_changes?since=now&include_docs=true 拉取增量,写入 Kafka,供 Flink 实时训练;CouchDB 本身只承担“顺序写+顺序读”职责,不扛分析流量,避免节点扩容成本。

通过以上三步,既利用 CouchDB 的多主复制实现“离线优先”,又用update_handler解决“全局顺序”,最后把分析流量卸到 Kafka,整套方案在 2023 年某头部外卖小程序实测:

  • 单节点 4C8G 支撑 1.2 万 QPS 写入,P99 延迟 38 ms;
  • 移动端日均流量从 3.8 GB 降到 1.1 GB;
  • 安全审计方抽查 6 个月行为链,顺序零丢失,一次过审。

拓展思考

  1. 如果业务要求“全局绝对时钟”,而不仅“单用户有序”,可在 update_handler 里把 NTP 时间戳与序列号同时写入,再用雪花算法把 41 位毫秒 + 10 位机器号 + 12 位序号压成 64 位 long,存成 globalSnowId,查询时直接按字符串排序即可,跨用户也保序
  2. 当用户量突破 5000 万,单库 _changes feed 过大,可按“用户尾号分库”做 16 个 Shard,每个 Shard 独立复制;客户端根据 userId%16 选择同步地址,横向扩容无单点。
  3. 国内金融场景需要“不可篡改”,可把每次行为的 _id、_rev、globalSeq 拼成字符串,调用国密 SM3 算哈希,再写入区块链存证平台(如 BSN 武汉链),实现“链上哈希+链下文档”双轨,既降低存储成本,又满足央行金融数据安全规范 5 级要求。