logo

收藏功能技术实现与架构设计指南

作者:很菜不狗2026.11.11 07:28浏览量:1

简介:本文从收藏功能的核心价值出发,系统讲解其数据建模、接口设计、高并发写入、分页查询与缓存策略,并给出可落地的代码示例与避坑建议。读者可掌握一套可扩展、易维护的收藏系统设计方法,提升开发效率与系统稳定性。

收藏功能的技术实现与架构设计

收藏是内容型产品中最基础也最容易做“歪”的功能之一。看似只是“用户与内容之间的一条记录”,实际涉及写扩散与读扩散的取舍、计数一致性、分页稳定性、幂等设计以及冷热数据分离等一系列工程问题。本文从需求本质出发,逐层拆解收藏功能的数据模型、接口设计、并发控制与演进路径,给出一套可直接落地的技术方案。

一、需求本质与边界界定

收藏功能的表层需求是“让用户把感兴趣的内容存起来,之后能找到”。但拆解到技术层,至少包含四个子问题:

  1. 关系存储:谁收藏了什么,何时收藏,是否取消过。
  2. 状态查询:某条内容是否被当前用户收藏,用于前端按钮态渲染。
  3. 列表检索:按收藏时间倒序列出某用户的收藏内容,并支持分页。
  4. 计数统计:某条内容被多少人收藏,用于内容侧展示热度。

需要提前明确的边界包括:收藏是否支持分组/文件夹、是否允许重复收藏同一条内容、取消收藏后是否保留历史记录、收藏列表是否需要按内容类型过滤。这些边界直接决定数据模型的设计,建议在开发前与产品侧书面确认,避免后期返工。

二、数据模型设计

2.1 核心关系表

最朴素的做法是一张关系表:

  1. CREATE TABLE user_favorite (
  2. id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  3. user_id BIGINT UNSIGNED NOT NULL,
  4. target_type TINYINT UNSIGNED NOT NULL COMMENT '内容类型:1文章 2视频 3商品',
  5. target_id BIGINT UNSIGNED NOT NULL,
  6. status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1有效 0已取消',
  7. created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  8. updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  9. UNIQUE KEY uk_user_target (user_id, target_type, target_id),
  10. KEY idx_user_time (user_id, status, created_at)
  11. ) ENGINE=InnoDB;

几个关键决策值得说明:

  • 唯一索引兜底幂等:uk_user_target 保证同一用户对同一内容只有一条记录,重复收藏请求在数据库层被拦截,避免应用层并发导致的脏数据。
  • 软删除而非物理删除:用 status 字段标记取消,保留历史轨迹,便于风控与行为分析;若数据量敏感,也可改为物理删除并在归档表中留痕。
  • 联合索引顺序:idx_user_time 以 user_id 打头、status 居中、created_at 收尾,正好覆盖“查某用户有效收藏按时间倒序”的主查询。

2.2 计数表的取舍

内容侧的收藏数有两种实现路径:

  • 实时聚合:每次查询对关系表做 COUNT,实现简单但代价高,内容列表页会迅速打爆数据库。
  • 独立计数:维护一张计数表或计数字段,写入时增减,查询时直接读取。

推荐后者,并采用“异步校准”策略:

  1. CREATE TABLE target_favorite_count (
  2. target_type TINYINT UNSIGNED NOT NULL,
  3. target_id BIGINT UNSIGNED NOT NULL,
  4. fav_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
  5. updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  6. PRIMARY KEY (target_type, target_id)
  7. ) ENGINE=InnoDB;

写入侧通过 INSERT ... ON DUPLICATE KEY UPDATE fav_count = fav_count + 1 完成原子增减;同时保留一条离线校准任务,定期以关系表为准修正计数偏差,防止因异常路径导致的长期漂移。

三、接口设计与幂等控制

3.1 接口语义

建议将收藏与取消收藏设计为两个独立接口,而非一个 toggle 接口:

  1. POST /api/favorite // 收藏
  2. DELETE /api/favorite // 取消收藏
  3. GET /api/favorite/state // 查询状态
  4. GET /api/favorite/list // 分页列表

toggle 接口在弱网重试场景下语义不明确——客户端重发可能把“已收藏”又切成“未收藏”。独立接口配合幂等语义更安全:重复收藏返回成功且不改变数据,重复取消同理。

3.2 幂等实现

幂等的核心是“操作结果与执行次数无关”。对收藏而言,利用唯一索引 + INSERT IGNORE 或捕获唯一键冲突即可:

  1. public boolean favorite(long userId, int targetType, long targetId) {
  2. try {
  3. favoriteMapper.insert(userId, targetType, targetId);
  4. countService.increment(targetType, targetId, 1);
  5. return true;
  6. } catch (DuplicateKeyException e) {
  7. // 已存在有效记录,视为成功,不重复计数
  8. return true;
  9. }
  10. }

注意计数增减必须与关系写入保持“同一业务事实”,否则会出现“记录存在但计数为 0”的不一致。常见做法是仅在插入真正生效时递增计数,取消时仅在状态由有效变为无效时递减。这要求存储层能返回“本次操作是否改变了状态”,例如使用带条件的 UPDATE ... WHERE status = 1 并检查影响行数。

3.3 状态查询的批量优化

列表页往往需要为每条内容渲染收藏按钮态,逐条查询状态会产生 N 次请求。应提供批量接口:

  1. POST /api/favorite/state/batch
  2. body: { "targetType": 1, "targetIds": [101, 102, 103] }
  3. resp: { "101": true, "102": false, "103": true }

服务端一次 IN 查询即可返回,配合本地缓存可进一步降低存储压力。

四、分页查询的稳定性问题

收藏列表按 created_at 倒序分页时,若用户在浏览过程中新增或取消收藏,传统 LIMIT offset, size 会出现重复或漏项。推荐使用游标分页(keyset pagination):

  1. SELECT target_type, target_id, created_at
  2. FROM user_favorite
  3. WHERE user_id = ?
  4. AND status = 1
  5. AND (created_at, id) < (?, ?)
  6. ORDER BY created_at DESC, id DESC
  7. LIMIT ?;

以 (created_at, id) 作为复合游标,既保证顺序唯一(同一秒内多条记录用 id 兜底),又避免深分页的偏移扫描。前端只需透传上一页最后一条的游标即可。

若产品强依赖页码跳转,则需接受一定的不精确性,并在接口文档中明确说明;或采用“快照 + 增量”的折中方案,在会话内固定数据集。

五、高并发写入与热点问题

收藏是典型的“写多读也多”场景,热点内容可能在同一时刻被大量用户收藏,计数行成为行锁争抢点。可采取以下分层策略:

  1. 计数分片:将单个计数拆为 N 个分片行,写入按 hash(userId) % N 落片,读取时求和。牺牲少量读性能换取写入吞吐。
  2. 异步合并:写入侧先落消息队列,由消费者批量合并计数更新,降低数据库压力;代价是计数存在秒级延迟,需在产品上接受“最终一致”。
  3. 本地聚合:单机内先做短窗口聚合再批量落库,适合突发流量削峰。

选择哪种取决于业务对计数实时性的要求。内容热度展示通常可接受秒级延迟,异步方案性价比最高;若涉及交易或权益,则必须强一致,应优先保证正确性而非吞吐。

六、缓存策略与一致性

收藏状态是典型的“读多写少、按用户维度聚合”的数据,适合多级缓存:

  • 本地缓存:缓存用户最近的状态查询结果,设置较短 TTL(如 30 秒),降低远程调用。
  • 分布式缓存:以 user_id + target_type + target_id 为键缓存布尔状态,写入时删除对应键(Cache Aside)。
  • 列表缓存:缓存首页收藏列表,翻页时按游标增量加载,避免整列表失效。

一致性上遵循“先更新数据库,再删除缓存”,并对删除失败做重试补偿。对于计数类数据,可接受短暂不一致,通过前述校准任务兜底。

七、演进路径与工程建议

一个可持续演进的收藏系统通常经历三个阶段:

  1. 单表阶段:关系表 + 计数表,满足基本功能,重点把幂等与索引做对。
  2. 读写分离阶段:列表查询走只读副本,写入走主库,缓解读压力。
  3. 分层存储阶段:热数据(近期收藏)留在行存,冷数据归档到列存或对象存储,列表查询按时间范围路由。

工程上还有几点容易被忽视:

  • 内容删除后的悬垂引用:内容下架后,收藏记录仍指向不存在的目标。列表查询需做存在性过滤,或在下架时异步清理并补偿计数。
  • 权限与隐私:收藏列表默认仅本人可见,若支持公开需单独授权模型,避免越权读取。
  • 可观测性:为收藏、取消、计数变更埋点,监控成功率与耗时;对计数偏差设置告警阈值。

结语

收藏功能的难点不在“存一条记录”,而在幂等、计数一致性、分页稳定性与热点写入这几个交叉点上。把数据模型的唯一约束设计对、把计数与关系写入绑定为同一业务事实、用游标分页替代偏移分页,这三点做到位,后续的缓存与分片才有稳固的地基。建议在首版就补齐幂等与批量状态查询能力,避免上线后因重试与 N+1 查询被迫重构。

发表评论

活动