SWE / Ads · System Design

TikTok 群聊设计:500 人通知与最近 40 条消息怎样快速加载?

分离持久化消息、最近消息缓存、在线推送与未读游标,处理重连、热点群和重复发送。

题目重点:分离持久化消息、最近消息缓存、在线推送与未读游标,处理重连、热点群和重复发送。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练采用每群内部单调序号,消息在持久化成功后才ACK;是否需要跨群顺序、撤回/编辑需另确认。
  • 最近窗口取40条,在线WebSocket推送,离线保留未读游标;Redis是可重建缓存。
  • 容量示例假设消息正文平均1KB、每群每秒10条、80%成员在线;这些数值用于计算,不是实际业务流量。

解题路线

  1. API拆send(group_id,client_msg_id,body)、history(group_id,before_seq,limit)、read_cursor(group_id,user_id,last_read_seq)。服务端核成员资格,client_msg_id在用户/群作用域幂等,不以请求到达次数发多条。
  2. 持久化Messages(group_id,seq,message_id,sender,body)与可重放发布事件。事务或outbox保证已ACK消息最终能通知;不在数据库写失败时先向500人发送“成功”消息。
  3. Redis按群缓存最新40条及版本水位,读取缺口回源数据库补齐。缓存顺序按seq,允许重复消息按ID去重;并发填充旧结果不能覆盖新水位,采用版本比较或同分区串行更新。
  4. 在线gateway维护用户连接与群订阅,按群事件扇出给在线成员;送达ACK和用户已读不同。离线推送可以合并成“群有新消息”,不为每条消息立即发500次手机通知。
  5. 重连客户端携带last_received_seq拉取缺口,推送与补拉可能重叠,按消息ID/seq去重。Redis失效时不丢持久化历史;旧连接未退出仍订阅时需要连接epoch或状态清理。
  6. 500人并不自动使每群读写都成为瓶颈。假设10msg/s与400在线成员,扇出约4000条/s、正文约4MB/s,另加协议开销;先限制每连接缓冲与慢消费者,再考虑群分区和gateway扩容。
  7. 最近40条正文约40KB/群;百万活跃群约40GB仅正文,不能忽略对象、索引、副本和Redis淘汰。LRU是淘汰策略,Redis是缓存服务,二者不是互斥选项。验收关注消息不丢、延迟分位、重连补齐与热点慢用户隔离。

手动走一遍

消息seq101已落库但gateway发布前崩溃,outbox重放使101最终通知;客户端在重连补拉100..103后又收到101推送,只渲染一次。最近缓存只有120..159,客户端要求110之前的历史必须回源,不能把缓存缺失解释为无消息。

规模与取舍

发送持久化按分区索引/事务成本计,在线扇出O(在线成员数),历史查询约O(log M+limit)。最近缓存O(活跃群数×40×平均消息大小),未读游标O(群成员关系数);不能每用户缓存完整群历史而不计算放大。

易错点与追问

容易失分的边界

  • Redis成功即认为消息可靠。
  • 每成员一份完整消息历史。
  • 未读和送达混成一个状态。
  • 重连补拉与推送重复渲染。
  • 慢连接队列无限增长。

继续深挖

  • 撤回消息怎样同步到离线设备?
  • 退出群之后还能否查旧历史?
  • 同群每秒十万消息会卡在哪里?
  • 多设备已读游标怎样合并?

技术参考

  • RFC6455 WebSocket协议:传输机制参考;应用消息持久化、顺序、重连补齐由本文设计定义。
✓已复制微信号!