6.聊天系统离线私聊消息设计

  1. 6.聊天系统离线私聊消息设计
    1. 6.1 基础版离线消息投递
    2. 6.2 一次性拉取所有离线
    3. 6.3 分页拉取
    4. 6.4 应用层 ACK 保证不丢
    5. 6.5 合并 ACK 请求
    6. 6.6 总结

6.聊天系统离线私聊消息设计


6.1 基础版离线消息投递

最容易想到的离线消息投递流程,如下所示:

  1. A → Server → B
    • 步骤 1:A 发送私聊消息到 Server。
    • 步骤 2:Server 检查 B 在线状态,判定为 offline
    • 步骤 3:Server 将消息存储到数据库(离线库)中。
    • 步骤 4:Server 返回给 A 发送成功(附带消息 ID,避免重复)。

  1. B 登录后拉取
    • 步骤 1:B 登录后,向 Server 拉取离线消息。
    • 步骤 2:Server 从数据库中读取所有离线消息。
    • 步骤 3:Server 删除数据库中的这些离线消息。
    • 步骤 4:Server 将消息返回给 B 客户端。

这种实现虽然简单,却存在以下问题:

  • 若 B 有大量好友,拉取次数与好友数成正比,效率低下。如果有一万个好友,难道要拉一万次?

6.2 一次性拉取所有离线

为减少登录时的交互次数,改为“按接收者 ID 一次性拉取”:

  • 批量拉取:仅用 receiveId = B 一次性查询,不再用 (senderId, receiveId) 复合索引查询。
  • 本地过滤:客户端拿到所有离线消息后,再根据 senderId 做本地分组或排序。

优点:

  • 登录时只需一次网络交互。
  • Server 端查询更简单快速。

缺点:

  • 如果 B 有成千上万条离线消息,一次性返回会造成单次包过大、网络延迟显著上升。

6.3 分页拉取

面对庞大消息量,采用分页策略:

  1. 请求第 1 页

    • B 请求最新 N 条(page=1, size=N)。
    • Server 返回条目 1…N。
  2. 请求第 2 页、N 页

    • B 再次请求 page=2, page=N,Server 返回相应片段。

优点:

  • 控制单包大小,避免网络拥塞或超时。
  • 客户端可渐进式加载,提升用户体验。

缺点:

  • 先删除,再返回,一旦中间环节(Server 崩溃、路由器丢消息,客户端Crush,网络断开)就会导致消息“丢失”。

6.4 应用层 ACK 保证不丢

为防止“删除后未送达”导致的丢失,引入应用层确认(ACK):

  1. 拉取时不删除:Server 先查询并返回消息及其 messageId不立即删除
  2. 客户端确认:客户端收到本页消息后,发送一条 ACK 请求,包含成功接收的 messageId 列表。
  3. Server 删除:Server 在收到 ACK 后,再根据 ID 列表删除对应离线记录。

此步保证:

  • 如果 Server 或客户端在返回过程中崩溃,消息依然保留,下次仍可拉取。

缺点:

  • 为了避免大的报文,离线消息要分页拉取,每次又必须ACK,客户端与服务端的交互次数加倍

6.5 合并 ACK 请求

优化思路:

  • 分页拉取带来额外 ACK 交互,为减少次数,可在“拉取 N 页时 ACK N–1 页”:
  • 拉取第二页消息的时候,实际上相当于第一页消息的ACK。拉取第N页消息的时候,实际上相当于第N减一页消息的ACK。如此一来,只会多一个ACK请求,与服务器多一次交互。
sequenceDiagram
  autonumber
  participant B_Client as B
  participant Server

  B_Client->>Server: PullOffline(page=1)
  Server-->>B_Client: Msgs1 + IDs1

  B_Client->>Server: PullOffline(page=2, ack=IDs1)
  Server->>DB: Delete(IDs1)
  Server-->>B_Client: Msgs2 + IDs2

  B_Client->>Server: PullOffline(page=3, ack=IDs2)
  Server->>DB: Delete(IDs2)
  Server-->>B_Client: Msgs3 + IDs3
  • 第 1 页:单独拉取,无 ACK。
  • 第 N 页:既拉取新消息,也 ACK 上一页,减少一次交互。

以下是对总结段落的优化,使其更简洁、条理清晰,并更贴合技术博客风格:


6.6 总结

在设计离线私聊消息投递机制时,我们通过多阶段的优化手段,实现了性能与可靠性的平衡:

  1. 一次性拉取

    • 核心思路:以接收者(B)的维度,一次性查询并获取所有离线消息,避免重复的 (senderId, receiveId) 联合查询,从而将登录时的网络交互次数降至最低。
    • 适用场景:离线消息量中等时,单次数据包大小可控。
  2. 按需拉取

    • 核心思路:客户端先拉取最新 N 条消息,再根据业务需求决定是否继续加载。这种“先看最新、后看历史”的策略是移动端常见的优化,能够快速满足用户对最新内容的即时需求。
  3. 分页拉取

    • 核心思路:将所有离线消息分为多页,逐页获取,每页大小可配置,既能避免单包过大,又能逐步回填历史消息。
    • 优点:控制响应包大小,减轻网络与客户端渲染压力;支持可视化进度反馈。
  4. 应用层 ACK 确认

    • 核心思路:在服务器返回离线消息后,不立即删除数据库中的记录,而是等待客户端发回 ACK(确认接收)后,再统一清除对应消息。
    • 作用:“先送达、后删除”,保证即便在中间环节(服务器崩溃、路由丢包、客户端崩溃等)出现故障,也不会丢失未被确认的离线消息。
  5. 合并 ACK 优化

    • 核心思路:将“拉取第 N 页”与“确认第 N–1 页”合并到同一次请求中。例如,客户端在请求第 3 页时附带对第 2 页的 ACK,从而每次仅产生一次额外的 ACK 交互,显著降低网络往返次数。
    • 适用场景:高并发用户登录或批量历史消息回溯时,交互次数优化收益显著。
  6. 渐进式演进

    • 最初的朴素方案,到批量拉取分页拉取应用层 ACK,再到合并 ACK,每一步都在提升系统吞吐量的同时,强化可靠性保障。
    • 这种“多阶段迭代”模式,既能快速上线最基础功能,又能在后续版本中平滑地引入更细粒度的性能和健壮性优化,为大规模在线游戏或社交应用提供了一条可持续演进的技术路线。

通过上述五个阶段的设计和优化,我们实现了:

  • 最少网络交互(一次性拉取);
  • 可控的数据量(分页拉取);
  • 端到端可靠性(应用层 ACK);
  • 最小化交互开销(合并 ACK)。

这样不仅满足了高并发环境下的性能需求,也确保了离线私聊消息在各种异常条件下都能“绝不丢失”,为企业级聊天系统奠定了坚实的基础。



转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏