如何存储用户行为序列并保证顺序?
解读
国内业务场景里,用户行为序列(点击、曝光、支付、分享等)既是实时推荐系统的“燃料”,也是风控、运营回溯的核心证据。面试官问“怎么存、怎么保序”,想确认三件事:
- 你是否理解 CouchDB 的最终一致性与多主复制模型,知道它在“顺序”这件事上的天然短板;
- 能否在“文档数据库”范式内,用_id、_rev、附件、本地序列等原生机制,设计出既高可用又低成本的可落地方案;
- 是否具备“离线优先→移动断网→增量同步→云端归并”的完整闭环思维,这是国内 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:客户端离线写入
- 本地维护一个 _local/seq 文档,保存“已同步序号”和“待同步队列”。
- 新行为产生时,先写进 IndexedDB(PouchDB),_id 格式:
behavior::<userId>::<ts>::<nanoId>
其中 ts 取当前毫秒,nanoId 用 6 位随机串防碰撞,保证单用户内严格递增。 - 队列满 100 条或网络恢复时,批量 PUT,一次请求压缩成 gzip,节省 70% 流量,符合国内运营商套餐敏感场景。
阶段 2:云端集中序号
- 设计 update_handler 函数
addSeq,接收行为 JSON,原子读取“用户最新序号”并 +1,返回带globalSeq的新文档。 - 同一用户并发写时,CouchDB 自动产生冲突分支;后台定时任务用
/_find?conflicts=true抓取冲突,按业务时间戳保留最新,其余写入冷存库(按年分区),满足网络安全法 6 个月日志留存要求。 - 对外查询提供两层索引:
- Mango 索引
{"userId":1, "globalSeq":1},用于“单用户顺序翻页”; - 视图
_design/behavior_count按天 reduce,给运营大屏秒级 PV/UV,避免全表扫描。
- Mango 索引
阶段 3:双向同步与审计
- 移动端用 PouchDB 的
live+retry模式,长连接断开后 5 秒自动重试,弱网场景下用户体验平滑。 - 服务端开启
require_valid_user,关闭_users匿名写,行为文档里带deviceIdHash字段,满足等保 2.0 个人信息脱敏要求。 - 每日凌晨用
/_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 个月行为链,顺序零丢失,一次过审。
拓展思考
- 如果业务要求“全局绝对时钟”,而不仅“单用户有序”,可在 update_handler 里把 NTP 时间戳与序列号同时写入,再用雪花算法把 41 位毫秒 + 10 位机器号 + 12 位序号压成 64 位 long,存成
globalSnowId,查询时直接按字符串排序即可,跨用户也保序。 - 当用户量突破 5000 万,单库 _changes feed 过大,可按“用户尾号分库”做 16 个 Shard,每个 Shard 独立复制;客户端根据
userId%16选择同步地址,横向扩容无单点。 - 国内金融场景需要“不可篡改”,可把每次行为的 _id、_rev、globalSeq 拼成字符串,调用国密 SM3 算哈希,再写入区块链存证平台(如 BSN 武汉链),实现“链上哈希+链下文档”双轨,既降低存储成本,又满足央行金融数据安全规范 5 级要求。