· 王思腾 · 系统架构 · 9 分钟阅读

系统设计:如何设计一个聊天系统(即时通讯 IM)

聊天系统看似简单(发消息、收消息),背后却要解决实时推送、消息有序、已读回执、离线消息、多端同步、海量连接等一系列难题。本文从收发消息的核心链路入手,讲清长连接网关、消息投递与确认、存储模型、群聊扇出,以及可靠性(不丢、不重、有序)的设计取舍。

聊天系统看似简单(发消息、收消息),背后却要解决实时推送、消息有序、已读回执、离线消息、多端同步、海量连接等一系列难题。本文从收发消息的核心链路入手,讲清长连接网关、消息投递与确认、存储模型、群聊扇出,以及可靠性(不丢、不重、有序)的设计取舍。

微信、钉钉、Slack 的核心都是 IM(即时通讯)。表面看就是“把 A 的消息送到 B”,但要做到实时、不丢、不重、有序,还要支持离线、多端、群聊,复杂度远超一个普通 CRUD 系统。

一、核心需求

  • 一对一聊天 + 群聊;
  • 实时投递:对方在线时毫秒级收到;
  • 消息可靠:不丢、不重复、同一会话内有序;
  • 在线状态:在线 / 离线 / 正在输入;
  • 离线消息:对方不在线时,上线后能拉到未读;
  • 多端同步:手机、电脑、网页消息一致,已读状态同步;
  • 支撑海量用户同时在线(百万级长连接)。

二、整体架构

IM 的关键分层:接入层维持长连接,逻辑层处理消息,存储层分“热消息”和“历史消息”。

flowchart TB
    subgraph Clients["客户端"]
        C1["手机"] --- C2["PC"] --- C3["Web"]
    end
    Clients <--> GW["接入层 · 长连接网关<br/>WebSocket / 自研 TCP 协议"]
    GW <--> Logic["逻辑层<br/>认证 · 路由 · 会话 · 消息处理"]
    Logic --> MQ["消息队列 Kafka"]
    MQ --> Push["投递服务"]
    Push --> GW
    Logic --> Hot[("Redis<br/>在线状态 · 未读计数 · 最新消息")]
    Logic --> Store[("消息存储<br/>HBase / Cassandra<br/>按会话分片")]
    Push --> Third["第三方推送<br/>APNs / 厂商通道(离线时)"]

三、长连接与接入层

聊天要实时,靠的是客户端与服务器之间的长连接(WebSocket,或移动端基于 TCP 的自研协议 + Protobuf):

  • 连接建立后保持,服务器有消息就主动推(Push),而不是客户端反复轮询(Pull,浪费流量和电量);
  • 单机维持长连接有上限(受文件描述符、内存限制,通常几万到几十万连接/机),所以接入层是无状态的连接网关集群,通过负载均衡摊开海量连接;
  • 网关只负责“维持连接、收发包、转发”,不存业务状态——哪台网关上有哪个用户的连接,记录在 Redis 的“路由表”里(用户 → 网关实例)。

心跳机制:客户端定时发心跳包,网关检测到连接断了就清理路由表;NAT 网络下心跳还能防止连接被运营商回收。

四、一条消息的完整旅程

A 给 B 发消息“你好”:

sequenceDiagram
    participant A as 客户端 A
    participant GW as 网关
    participant L as 逻辑层
    participant DB as 消息存储
    participant B as 客户端 B
    A->>GW: 发送消息(本地生成 msgId)
    GW->>L: 转发
    L->>L: 校验、分配全局 seqId、落库
    L->>DB: 持久化消息
    L-->>A: 返回 ACK(服务端收到)
    L->>B: B 在线则推送消息
    B-->>L: 返回 ACK(已收到)
    L->>A: 推送“已送达”回执
    Note over L,B: B 不在线 → 存离线消息 + 走 APNs/厂商推送

几个关键设计:

  • 消息 ID 与序列号:客户端先生成全局唯一 msgId(去重用,防重复发送);服务端在会话维度分配单调递增的 seqId(保证同一会话内消息有序、便于同步);
  • 两段 ACK:客户端→服务端 ACK 表示“服务器收到了”,接收方→服务端 ACK 表示“对方收到了”,据此显示“发送中 / 已送达 / 已读”;
  • 投递失败重试:推送后没收到对方 ACK,会重试;客户端按 msgId 去重,保证“至少一次投递 + 客户端去重 = 不重复”。

五、在线状态与离线消息

  • 在线状态存 Redis:用户 → 所在网关 / 在线状态。发消息时查路由表——在线就实时推,不在线就:
    1. 消息落库(历史存储);
    2. 在 Redis 记未读计数;
    3. 通过 APNs(iOS)/ 厂商通道(安卓) 发系统级推送,让 App 没启动也能收到通知;
  • 用户上线后,客户端带上本地“已同步到的最大 seqId”,向服务端拉取增量消息(seqId > 本地最大 的所有消息),完成离线补发和多端同步。

六、群聊:写扩散 vs 读扩散

群聊最大的难题是扇出(fan-out):一条群消息要让成百上千个成员看到。两种模型:

flowchart LR
    subgraph W["写扩散(推模式)"]
        W1["群消息"] --> W2["发消息时<br/>为每个成员复制一份<br/>写进各自收件箱"]
    end
    subgraph R["读扩散(拉模式)"]
        R1["群消息"] --> R2["只存群会话一份"]
        R3["成员读时<br/>拉取所有群的新消息合并"] --> R2
    end
  • 写扩散(推模式):发消息时给每个成员的收件箱写一份。读很快(只拉自己的收件箱),但写放大严重——一个万人群发一条要写一万次;
  • 读扩散(拉模式):消息只在群会话存一份,成员读取时拉取自己加入的所有群的新消息再合并。写轻松,但读放大、要合并排序。

实践中多为混合:小群用写扩散(成员少、写成本低、体验好),大群/超大群用读扩散(避免写爆)。

七、存储设计

消息数据量极大且写多读少、天然按时间有序,选型上:

  • 热数据(在线状态、未读数、最近消息、会话列表)放 Redis,追求低延迟;
  • 历史消息用 HBase / Cassandra 这类宽列存储:按 会话ID + seqId(时间) 做 rowkey,天然适合“某会话按顺序翻页”的海量写入与顺序读;可水平分片扩展;
  • 消息不可变(发出后不修改),很适合这种追加写的存储。

八、可靠性小结

问题解法
消息丢失服务端落库后才回 ACK;推送失败重试;离线走第三方推送
消息重复客户端生成唯一 msgId,接收端去重
消息乱序会话维度单调递增 seqId,客户端按序排列
连接海量无状态网关集群 + Redis 路由表 + 心跳保活
多端一致以服务端 seqId 为准,上线拉取增量同步

九、小结

  1. IM 分三层:长连接网关维持海量连接、逻辑层处理路由与会话、存储层分 Redis 热数据和 HBase/Cassandra 历史消息;
  2. 实时靠长连接推送,离线靠消息落库 + 未读计数 + APNs/厂商通道,上线按 seqId 拉增量实现补发与多端同步;
  3. 可靠性靠“至少一次投递 + 唯一 msgId 去重 + 会话内 seqId 保序”;
  4. 群聊扇出要在写扩散(读快写重)和读扩散(写轻读重)之间取舍,大小群用混合策略;
  5. 消息不可变、按会话顺序写,适合宽列存储水平扩展。

一个“能聊天”的 demo 一天能写完,一个“不丢消息、百万在线”的 IM 要打磨数年——差别全在这些可靠性和扩展性的细节里。

本文由 AI 辅助创作,所有内容仅供交流与参考。
分享:
返回博客
系统设计:如何设计搜索自动补全(搜索建议)系统

系统设计:如何设计搜索自动补全(搜索建议)系统

在搜索框敲下几个字,下拉框立刻给出最热门的补全建议——这个功能要求在几十毫秒内、从海量词条里返回“以当前输入为前缀、且最热门”的 Top-K。本文讲清前缀匹配的数据结构(Trie/字典树)、热度排序 Top-K、查询日志离线构建词典、在线低延迟服务与缓存,带你设计一个类 Google/淘宝的搜索建议系统。

系统设计:如何设计一个云盘(网盘)系统

系统设计:如何设计一个云盘(网盘)系统

云盘的核心不是“把文件存到服务器上”,而是支撑海量小文件与超大文件的上传下载、断点续传、秒传去重、分块存储与弹性扩容。本文从文件元数据与内容分离讲起,覆盖分块上传、秒传(内容哈希去重)、对象存储、目录树模型与分享权限,带你设计一个类百度网盘/Google Drive 的系统。

个人与小团队的全栈技术栈:前端、后端到持续部署,怎么做到低成本、快交付

个人与小团队的全栈技术栈:前端、后端到持续部署,怎么做到低成本、快交付

一个人或两三个人做产品,最大的约束不是技术,是时间和钱。这套栈的目标很明确:不养服务器、不写运维、Git push 就上线、流量小的时候几乎零成本。本文从前端静态优先、后端 Serverless、托管数据库,到 CI/CD 自动部署,给出一套能今天动手、明天上线、跑起来月账单趋近于零的务实组合,顺便拿这个网站本身当例子。