真实面经题目 · 原创解析

设计朋友圈功能时,数据库表如何从简单到复杂演进?

朋友圈数据库设计可以按复杂度分层回答:最小可用版本先有用户、好友关系、动态、媒体、评论和点赞;进阶版本补可见范围、权限校验和删除模型;高并发版本再讨论时间线、读扩散与写扩散、冷热数据、索引、分库分表、幂等与一致性。面试时不要一上来就堆表,而是先说明业务读写路径:发动态、刷列表、看详情、评论点赞、删除和权限变化。

出现于:阿里巴巴 · 后端开发

60 秒回答模板

我会从简单到复杂设计朋友圈。第一版保证核心功能可用:用户表存用户基础信息;关系表存好友或关注关系;动态表存正文、作者、状态、创建时间;媒体表一对多挂在动态下;评论表和点赞表分别记录互动。第二版补权限:动态需要可见范围字段或独立可见范围表,支持公开、好友可见、部分可见、不给谁看;查询动态和详情时都必须做权限校验。第三版考虑性能:朋友圈列表不能每次全表扫动态表,所以需要时间线模型。小规模可以读扩散,刷列表时按好友列表查动态;大规模可以写扩散,发布时把动态写入好友的收件箱时间线;也可以混合,普通用户写扩散,大 V 或好友数特别多的人读扩散。第四版考虑工程化:动态、评论、点赞要有合适索引;点赞要用唯一键保证幂等;删除要用软删除并级联隐藏媒体、评论、点赞;冷热数据按时间分层;数据量大后按 user_id 或 post_id 分库分表。最后补一致性:发布动态时动态表和时间线表可能异步写入,用消息队列、去重键、补偿任务保证最终一致;权限变更、删除动态时要让列表和详情权限保持一致。

考点 最小可用模型
主线 好友或关注关系
易错点 只设计动态表,不设计好友关系和可见范围,无法回答谁能看…

深入解析

01

最小可用模型

最简单的朋友圈可以抽象成 6 类表:用户表、好友或关注关系表、动态表、媒体表、评论表、点赞表。用户表只负责身份和展示信息,例如 user_id、nickname、avatar、status、created_at。动态表是核心主表,例如 post_id、author_id、content、post_type、status、created_at、updated_at。媒体表和动态是一对多关系,例如 media_id、post_id、media_type、url、width、height、sort_order。评论表记录 comment_id、post_id、user_id、parent_comment_id、content、status、created_at。点赞表记录 post_id、user_id、created_at,并用 post_id + user_id 做唯一约束,防止重复点赞。

02

好友或关注关系

如果是微信式朋友圈,关系更接近双向好友,可以设计 friend_relation 表:user_id、friend_id、status、created_at、updated_at,status 表示申请中、已通过、已拉黑、已删除。为了查询方便,双向好友通常会存两条有向记录,A-B 和 B-A 各一条,这样查某个用户的好友列表只需要按 user_id 查询。如果是订阅式关系,则是 follow_relation:follower_id、followee_id、status、created_at,本质是单向关注。

03

动态主表

动态表不要把所有内容都塞进一个大字段。核心字段包括 post_id、author_id、content、post_type、visibility_type、status、created_at、updated_at。post_type 可以区分纯文本、图片、视频、转发等;status 可以表示正常、作者删除、审核隐藏、系统删除。动态表的查询常见路径有按作者查个人主页动态、按 post_id 查详情、按时间范围查历史数据。

04

媒体表拆分

图片和视频不应该直接放在动态表里。动态表只保存文本和动态元信息,媒体表保存多媒体资源。这样一条动态可以有多张图片或多个视频,也方便做排序、封面、宽高、时长、转码状态等扩展。媒体表可以设计 post_id + sort_order 索引,用于按动态批量加载媒体。实际文件一般放对象存储,MySQL 只存 URL、资源 key、媒体类型和元信息。

05

评论和点赞

评论表通常按 post_id 查询,所以需要 post_id + created_at 索引。支持楼中楼时,可以用 parent_comment_id 表示回复哪条评论,也可以增加 root_comment_id 方便按一级评论聚合。点赞表需要处理高频写入和幂等,post_id + user_id 唯一键可以保证一个用户对一条动态只能点赞一次。为了列表页性能,可以冗余 like_count、comment_count,但明细表仍应作为事实来源。

06

可见范围

朋友圈的难点之一是权限。简单做法是在动态表放 visibility_type,例如 public、friends、private、partial_visible、partial_hidden。复杂做法增加 post_visibility 表,记录 post_id、target_user_id、rule_type,其中 rule_type 表示允许可见或禁止可见。权限判断必须出现在列表、详情、评论、点赞入口,不能只在列表过滤,否则用户拿到 post_id 后可能越权查看详情。

07

时间线模型

刷朋友圈本质是查询用户可见的动态流。小规模可以采用发件箱模式,也叫读扩散:用户打开朋友圈时,先查好友列表,再从动态表按 author_id in 好友集合和时间倒序查。这种方案写入简单,但好友很多时查询压力大。大规模可以采用收件箱模式,也叫写扩散:作者发动态后,把 post_id 写入每个可见用户的 timeline_inbox 表,用户刷列表时只查自己的 inbox。

08

混合扩散策略

读扩散适合用户关系少、发布频率高或系统早期阶段;写扩散适合读多写少、用户刷列表频繁的场景。真实系统更常见的是混合方案:普通用户发动态写入好友收件箱;好友数极多的用户不写扩散,刷列表时再临时合并这类用户的发件箱;私密动态或部分可见动态按权限规则写入指定用户 inbox。

09

删除和权限变化

删除动态一般使用软删除,动态表 status 改为 deleted,而不是物理删除。原因是评论、点赞、媒体、时间线里可能都有引用,软删除可以避免大量级联物理删除,也方便审计和恢复。若使用写扩散,删除时可以异步清理 timeline_inbox,也可以不删 inbox,只在回查动态详情时发现 deleted 后过滤。拉黑、删除好友、修改可见范围后,也要重新校验权限。

10

冷热数据

朋友圈强依赖时间顺序,天然适合冷热分层。最近一段时间的动态、评论、点赞属于热数据,需要高性能索引和缓存;较早数据属于冷数据,可以归档到历史表或低成本存储。时间线 inbox 也可以只保留最近 N 条或最近 N 天,更早内容回源动态表或历史库查询。评论和点赞计数可以放缓存或计数表,但需要从真实明细表中定期校准。

11

索引设计

索引要围绕查询路径设计。好友关系表需要 user_id + status + friend_id;动态表需要 author_id + status + created_at;媒体表需要 post_id + sort_order;评论表需要 post_id + status + created_at;点赞表需要 post_id + user_id 唯一键,也可以增加 user_id + created_at 用于查用户点赞历史;时间线表需要 owner_id + created_at 或 owner_id + timeline_id。分页要避免深分页,优先用游标分页。

12

分库分表

数据量变大后,动态、评论、点赞、时间线都会成为大表。动态表可按 author_id 分片,便于查某个作者的动态;评论和点赞可按 post_id 分片,便于查某条动态的互动;时间线 inbox 可按 owner_id 分片,便于查某个用户的朋友圈流。分片键要贴合最核心查询路径,否则会出现跨分片聚合。全局 post_id、comment_id 可以用雪花算法或号段服务生成。

13

幂等与一致性

发布动态、点赞、取消点赞、评论、删除都要考虑重复请求。点赞可通过唯一键天然幂等;发布动态可以由客户端或服务端生成 request_id,防止网络重试造成重复动态;评论也可以用 user_id + request_id 去重。动态主表写入成功后,写时间线、更新计数、发通知通常可以异步执行,使用消息队列保证最终一致,消费者也要支持幂等。

易错点

  • 只设计动态表,不设计好友关系和可见范围,无法回答谁能看见。
  • 把图片、视频直接塞进动态表,导致动态表过宽且扩展困难。
  • 点赞表不加 post_id + user_id 唯一约束,重复请求会产生脏数据。
  • 只在列表页做权限过滤,详情页、评论、点赞入口没有二次校验。
  • 用 offset 做深分页,数据量大后刷朋友圈性能明显下降。
  • 写扩散时没有考虑大用户,导致一个用户发动态触发海量写入。
  • 删除动态时物理删除主表,导致评论、点赞、媒体、时间线引用混乱。
  • 冗余计数后没有重试、补偿和校准机制,点赞数评论数容易长期错误。
  • 分库分表只按主键随机分片,没有贴合 author_id、post_id、owner_id 等查询路径。
  • 没有说明一致性模型,默认所有表同步强一致,实际高并发场景很难落地。

面试官追问

如果用户有 5000 个好友,刷朋友圈怎么查?

不建议每次直接用 author_id in 5000 去动态表查,这会带来很大的索引扫描和排序压力。可以使用 timeline_inbox 表,发布动态时把 post_id 写入可见好友的 inbox,用户刷列表时只按 owner_id 倒序查自己的时间线。对于好友数特别大的作者,可以采用混合策略,不做全量写扩散,读取时再合并其发件箱。

部分可见和不给谁看怎么设计?

动态表用 visibility_type 表示权限类型,复杂名单放 post_visibility 表。比如 partial_visible 表示只给名单内用户看,partial_hidden 表示名单内用户不可见。查询时先判断动态状态和作者关系,再根据 visibility_type 到明细表判断当前用户是否有权限。

点赞表为什么要加唯一键?

因为点赞是高频操作,客户端可能因为网络重试重复请求。如果没有 post_id + user_id 唯一键,同一个用户可能对同一条动态产生多条点赞记录,导致点赞数错误。唯一键可以让点赞天然幂等,重复点赞时返回成功或忽略即可。

删除好友后还能看到以前的朋友圈吗?

这取决于产品规则,但数据库设计上必须支持重新校验权限。如果规则是不再可见,那么即使 timeline_inbox 里还有历史 post_id,详情回查时也要根据当前好友关系过滤掉。如果规则是历史可见,则需要在时间线或权限快照里保留当时的可见关系。面试中可以先说明规则假设,再给出对应设计。

动态删除后要不要清理所有人的时间线?

可以异步清理,但不一定要同步清理。更稳妥的做法是动态表先软删除,列表和详情读取时过滤 deleted 状态;后台任务再慢慢清理 timeline_inbox。这样删除操作响应快,也不会因为清理大量收件箱导致超时。

评论和点赞数如何保证准确?

明细表是准确来源,计数字段是性能优化。新增评论或点赞时先写明细,再异步更新计数;失败时通过消息重试或补偿任务修复。对一致性要求更高的详情页,可以实时查明细或用定期对账修正计数。

时间线表应该存完整动态内容还是只存 post_id?

通常只存 owner_id、post_id、author_id、created_at、排序 id 和少量冗余字段。完整内容仍在动态表和媒体表中,避免作者编辑、删除、审核状态变化后多处数据不一致。如果列表性能要求极高,可以适度冗余摘要,但要接受最终一致和失效更新成本。

如何处理发布动态和写入时间线之间的不一致?

可以先写动态主表,再发送消息异步写 timeline_inbox。消息里带 post_id 和幂等键,消费者重复执行不会产生重复时间线记录。若消息失败或消费者异常,通过重试队列和补偿任务扫描未扩散完成的动态,保证最终一致。