私信是社交产品里「最私密、最难做」的一环:它要求消息不丢、不重、不乱序,还要求别人(包括平台自己)看不到内容。这三个诉求互相拉扯——强一致好做但延迟高,端到端加密安全但难做多端同步与检索。本文讲透私信的工程实现:会话模型、消息时序、E2EE 密钥体系、多端同步、可靠投递、回执一致性、元数据保护与合规边界。
前置:/miniblog-auth-session/(身份与设备会话)、/miniblog-notification-system/(推送与触达)、/miniblog-cross-device-sync/(多端状态同步)。
目录
- 1. 私信的挑战:可靠、有序与隐私三角
- 2. 会话模型:单聊、群聊与消息存储
- 3. 消息时序:单调递增序号与因果序
- 4. 端到端加密:密钥交换与双棘轮
- 5. 多端同步:设备列表与密钥分发
- 6. 消息投递:长连接、推送与离线补偿
- 7. 已读回执与在线状态的一致性
- 8. 安全边界:元数据、备份与举报
- 9. 存储与合规:加密数据的可检索困境
- 10. 速查表与一句话记忆
- 延伸阅读
1. 私信的挑战:可靠、有序与隐私三角
私信系统要在三个维度同时达标,任何一维妥协都会立刻被用户感知。
三个硬指标:
□ 可靠性:消息不丢(断网、杀进程、切设备都不丢)
□ 有序性:同一会话内消息顺序在所有端一致
□ 隐私性:服务端只见密文,内容不可读
三个软指标:
□ 低延迟:P99 送达 < 500ms(在线)
□ 多端一致:手机、平板、Web 看到的会话状态一致
□ 可恢复:换机/重装后历史可重建
三角的核心矛盾在于「服务端能不能读」。E2EE 让服务端只剩密文,于是排序、去重、回执、检索这些原本靠服务端理解内容就能做的事,全部要重新设计成「只依赖元数据」的方案。
| 能力 | 明文方案 | E2EE 方案 |
|---|---|---|
| 排序 | 服务端序号 | 客户端序号 + 服务端元数据 |
| 去重 | 内容哈希 | 消息 ID + 发送方随机数 |
| 检索 | 全文索引 | 本地索引或加密索引 |
| 回执 | 服务端聚合 | 逐设备加密回执 |
2. 会话模型:单聊、群聊与消息存储
会话(conversation)是私信的基本容器,单聊与群聊在数据模型上应尽量统一。
-- 会话表
CREATE TABLE conversation (
conv_id BIGINT PRIMARY KEY,
type TINYINT, -- 1 单聊 2 群聊
owner_uid BIGINT, -- 单聊时归一化后的 owner,避免双份
created_at BIGINT,
last_msg_id BIGINT,
member_count INT
);
-- 成员表(含未读游标)
CREATE TABLE conversation_member (
conv_id BIGINT,
uid BIGINT,
read_cursor BIGINT, -- 已读到的消息序号
mute TINYINT,
joined_at BIGINT,
PRIMARY KEY (conv_id, uid)
);
单聊的经典坑是「双份会话」:A 给 B 发消息时若各建一条,就会出现两个会话 ID。解决方式是归一化 owner:conv_id = hash(min(uidA,uidB), max(uidA,uidB)),双端共享同一会话。
群聊成员上限决定架构:小群(<500)可以用「写扩散」——消息写一份、成员各自维护游标;大群(>5000)必须用「读扩散」——消息只存一份,成员按需拉取。两者的分界点由「写放大系数 × 成员数」决定。
写扩散成本 = 成员数 × 消息数
读扩散成本 = 拉取次数 × 单次消息数
选择:成员数 × 发送频率 > 阈值 → 读扩散
3. 消息时序:单调递增序号与因果序
分布式系统里「时间」不可靠,因此私信排序不能依赖物理时钟,要用逻辑序号。
序号设计:
seq = 会话内单调递增整数,由服务端分配(单聊/群聊统一)
client_msg_id = 客户端生成的 UUID,用于去重与回执匹配
排序规则:同一会话内按 seq 升序;客户端本地先乐观插入(用 client_msg_id 占位),收到服务端回执后用 seq 修正位置。
| 场景 | 排序依据 | 处理 |
|---|---|---|
| 同会话同端 | seq | 直接排序 |
| 同会话多端 | seq | 服务端为准 |
| 跨会话(引用) | 引用消息的 seq | 拉取被引用消息 |
| 时钟漂移 | 忽略物理时钟 | 只信 seq |
E2EE 下服务端无法读内容,但仍可分配 seq——因为 seq 是元数据。真正的难点是因果序:群聊里「回复某条消息」需要表达因果关系。做法是消息携带 reply_to_seq,客户端渲染时按因果链展示,而不是按到达顺序。
因果链渲染:
msg.reply_to_seq 存在 → 先渲染被引用消息 → 再渲染当前消息
形成缩进或引用块,保证跨端一致的对话语义
4. 端到端加密:密钥交换与双棘轮
E2EE 的两块基石是「如何协商密钥」与「如何持续换钥」。
密钥协商:Signal 的 X3DH 协议用「身份密钥 + 一次性预密钥 + 签名预密钥」组合出共享密钥,即使长期密钥泄露,历史会话仍安全(前向保密)。
X3DH 输入:
IK_A A 的身份私钥
EK_A A 的临时私钥
SPK_B B 的签名预密钥(长期)
OPK_B B 的一次性预密钥(用完即弃)
输出:SK = KDF(DH1 ‖ DH2 ‖ DH3 ‖ DH4)
持续换钥:双棘轮(Double Ratchet)由「DH 棘轮」与「对称棘轮」组成——每收到对方新的 DH 公钥就推进 DH 棘轮,每条消息推进对称棘轮,从而保证「一条密钥只加密一条消息」,实现后向保密(Post-Compromise Security)。
双棘轮状态机:
发送:chain_key → KDF → message_key + next_chain_key
接收:收到新 DH 公钥 → 推进 DH 棘轮 → 重置接收链
| 性质 | 依赖机制 | 保障 |
|---|---|---|
| 前向保密 | DH 棘轮 + 临时密钥 | 旧密钥泄露不影响历史 |
| 后向保密 | 对称棘轮逐条换钥 | 单条泄露不影响后续 |
| 身份认证 | 签名预密钥 + 指纹校验 | 防中间人 |
客户端应展示「安全码」(指纹),让用户可线下核对,这是对抗服务端作恶的最后一道防线。
5. 多端同步:设备列表与密钥分发
E2EE 下,一条消息要给「收件人的所有设备」各加密一份,因此设备列表与密钥分发是核心。
发送流程(1 对 N 设备):
1. 拉取接收方设备列表(device_id 列表)
2. 对每个设备执行一次棘轮加密 → 密文列表
3. 服务端按设备路由,各设备只解自己的密文
多端同步的关键设计:
- 设备信任链:新设备加入必须由已有设备批准(扫码/确认),否则不能解密历史。
- 发送方设备同步:自己发的消息也要加密给「自己的其他设备」,否则换机看不到自己发的内容。
- 历史消息同步:新设备默认只看新消息;要看历史需触发「历史密钥传递」,由旧设备解密后重新加密给新设备。
设备列表变更 → 生成新会话密钥 → 用每台设备的公钥封装 → 各端解封
关键:设备列表变更后必须「换钥」,否则被移除的设备仍能解密
| 事件 | 处理 | 风险 |
|---|---|---|
| 新增设备 | 旧设备批准 + 换钥 | 未批准设备不可解密 |
| 移除设备 | 立即换钥 | 旧设备无法解新消息 |
| 设备丢失 | 撤销 + 全量换钥 | 需重新验证安全码 |
6. 消息投递:长连接、推送与离线补偿
投递链路要覆盖「在线、后台、离线」三种状态。
在线:WebSocket 长连接直投(延迟最低)
后台:厂商推送通道(APNs/FCM/厂商通道)唤醒
离线:消息落库,待上线后按 seq 增量拉取
可靠性靠三段式确认:客户端收到消息回 ACK,服务端收到 ACK 才标记「已投递」,否则在超时后重投。
投递状态机:
SENT(服务端已收) → DELIVERED(对端已收 ACK) → READ(对端已读)
超时重投:DELIVERED 前,每隔 T 秒重投,最多 N 次
去重:客户端按 client_msg_id / seq 去重,重复消息直接丢弃
离线补偿用「游标拉取」:客户端上报本地最大 seq,服务端返回 seq > cursor 的所有消息,分页拉取直到追平。注意拉取与实时推送可能重叠,必须靠 seq 去重,避免「拉取到的消息又被推送一次」。
| 状态 | 通道 | 保证 |
|---|---|---|
| 在线 | 长连接 | 秒级送达 |
| 后台 | 推送 | 唤醒后拉取 |
| 离线 | 落库 | 上线增量拉 |
| 弱网 | 重投 | 至少一次 + 去重 |
7. 已读回执与在线状态的一致性
回执和在线状态是「高频、小、多端」的典型场景,设计不当会放大成写风暴。
已读回执:read_cursor = max(已读 seq),只上报「游标」不上报「逐条」
在线状态:typing / online / last_seen,用 TTL 键 + 心跳维持
关键设计:
- 回执用游标而非逐条:只上报「我已读到 seq=1000」,服务端即可推断前 1000 条已读,避免逐条写。
- 在线状态用过期键:
SETEX presence:uid 30 "online",心跳续期,超时即离线,避免「僵尸在线」。 - typing 状态要节流:输入中状态只在变化时上报(开始/停止),且限频,否则每次按键都发一次。
| 状态 | 存储 | 过期 | 一致性 |
|---|---|---|---|
| 已读游标 | 成员表字段 | 永久 | 强一致(服务端为准) |
| 在线 | Redis TTL | 30s | 最终一致 |
| 输入中 | Redis TTL | 5s | 尽力而为 |
| 最后在线 | Redis/DB | 永久 | 最终一致 |
多端一致性要点:已读游标取「所有端的最大值」(一端读了就算读);在线状态取「任一端在线即在线」。这两个聚合规则要在服务端统一,避免各端各算。
8. 安全边界:元数据、备份与举报
E2EE 保护的是内容,但元数据(谁在什么时候和谁聊天)仍然暴露,这是必须明确告知用户的安全边界。
E2EE 能保护:消息内容、图片、文件(加密后上传)
E2EE 难保护:会话双方、时间戳、消息长度、设备信息
加固元数据的常见手段:
- 密封发送(Sealed Sender):发送者身份也加密,服务端只知「发给谁」不知「谁发的」。
- 填充与延迟:消息填充到固定长度桶,随机延迟发送,削弱流量分析。
- 匿名凭证:用匿名凭证发送,切断发送者与账号的直接关联。
备份是 E2EE 最尴尬的地方:云端备份若可被服务端解密,等于没有 E2EE。方案有三:
| 备份方案 | 服务端可读 | 换机恢复 | 代价 |
|---|---|---|---|
| 明文云备份 | 是 | 容易 | 丧失 E2EE |
| 端加密 + 密码 | 否 | 需密码 | 忘记密码即丢失 |
| 端加密 + 恢复码 | 否 | 需恢复码 | 用户易丢码 |
举报是合规刚需,但 E2EE 下服务端看不到内容。可行做法是「客户端举报」:用户主动选择要举报的消息,客户端把该条明文(连同密钥)交给审核系统——只举报、不扫描,兼顾隐私与合规。
9. 存储与合规:加密数据的可检索困境
E2EE 下服务端只剩密文,全文检索、内容审核、数据导出都失效,需要专门设计。
可检索加密的三种折中:
1. 本地索引:客户端建索引,只在本机可搜(隐私最好,换机丢)
2. 加密索引:倒排索引加密后上传,查询用可搜索加密(性能差)
3. 明文索引 + 端加密正文:索引泄露风险(不推荐)
工程上多数产品选「本地索引 + 服务端只存密文」:搜索在客户端完成,服务端只做同步。代价是换机后搜索索引要重建(拉全量密文重新索引),成本高但可接受。
数据导出与合规同样受限:GDPR 的「数据可携带」要求用户能导出自己的数据——导出的是密文,用户需要在客户端用自己的密钥解密。删除权则较好实现:删除会话时同步删除服务端的密文与元数据。
| 合规项 | 明文方案 | E2EE 方案 |
|---|---|---|
| 全文检索 | 服务端索引 | 客户端本地索引 |
| 内容审核 | 服务端扫描 | 客户端举报 + 被动抽检 |
| 数据导出 | 服务端导出 | 客户端解密导出 |
| 删除权 | 服务端删除 | 删除密文与元数据 |
| 执法调取 | 直接提供 | 只能提供元数据 |
工程要点:私信系统的设计顺序应是「先定隐私边界,再定数据模型」——确定服务端能看什么(元数据),不能看什么(内容),然后所有排序、去重、回执、检索都基于元数据重建。可靠投递靠三段式确认 + seq 去重,多端一致靠游标聚合规则统一,E2EE 靠 X3DH 协商 + 双棘轮换钥 + 设备信任链,元数据加固靠密封发送与填充。记住:E2EE 不是「加个加密库」,而是整套「服务端不再理解内容」的架构重构。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| 单聊会话怎么归一 | conv_id = hash(min,max),双端共享 |
| 消息怎么排序 | 会话内单调 seq,忽略物理时钟 |
| 怎么去重 | client_msg_id + seq 双保险 |
| 密钥怎么协商 | X3DH(身份+签名预密钥+一次性预密钥) |
| 怎么持续换钥 | 双棘轮:DH 棘轮 + 对称棘轮 |
| 多端怎么同步 | 设备列表 + 逐设备加密 + 变更即换钥 |
| 投递怎么可靠 | 三段式确认 + 超时重投 + seq 去重 |
| 回执怎么省 | 上报已读游标而非逐条 |
| 元数据怎么保护 | 密封发送 + 填充延迟 + 匿名凭证 |
| E2EE 怎么合规 | 客户端举报 + 本地索引 + 密文导出 |
一句话记忆:私信 = 归一化会话(hash min/max)+ 单调 seq 排序(不信时钟)+ client_msg_id 去重 + X3DH 协商(前向保密)+ 双棘轮换钥(后向保密)+ 设备信任链(变更即换钥)+ 三段式确认投递(至少一次 + 去重)+ 游标回执(省写)+ 密封发送与填充(护元数据)+ 客户端举报与本地索引(E2EE 下的合规折中)——在「可靠、有序、隐私」三角里,用元数据重建服务端能理解的一切。
延伸阅读
- /miniblog-auth-session/ — 身份认证与设备会话管理
- /miniblog-notification-system/ — 推送触达与到达率优化
- /miniblog-cross-device-sync/ — 多端状态与增量同步
- /miniblog-realtime-streaming/ — 长连接与实时链路
- /miniblog-multi-tenancy-isolation/ — 租户隔离与数据边界
- 产品专题
- 分布式系统专题
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。