默认
发表评论 4
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?别走弯路了... 找站长给点建议
请教关于IM消息有序性和消息同步问题
阅读(10620) | 评论(4 收藏 淘帖1
2金币
在阅读一些文章之后,产生了一些疑问:
1、通过唯一id+服务端生成的会话唯一seq来保证会话内消息有序,那么客户端离线同步是怎么做呢?客户端存储的已同步的最大消息id貌似就没有用了吧?

2、如果是写扩散模型,消息写入到用户信箱后,如何维护信箱的timeline呢?

3、如果是读扩散模型,客户端消息同步也无法知道已同步的最大消息id了吧,毕竟消息id不是有序,只在会话内有序?

4、维护全局唯一消息id可以解决上述问题,但id发号器压力会很大,如果一次取一个号段,集群部署的话如何保证分配id有序呢?


本人IM萌新,请各位大佬解答一下,谢谢!

上一篇:[讨论] 关于IM消息增量拉取问题,比如seq序号导致的收发顺序问题

本帖已收录至以下技术专辑

推荐方案
评论 4
引用:JackJiang 发表于 2024-11-07 10:11
我觉得这个系列文章应该能解决你的疑问,尤其是前两篇微信团队的文章:

这是阅读微信一些文章得出猜想,站长看看分析的是否有问题

1、微信是基于写扩散的,每个用户有自己的收信箱
2、微信的消息是会话内有序的,并不是全局有序
3、会话内有序会带来一个问题,用户信箱的数据是无序的,因为存的是多个会话的混合消息。这样就无法进行同步,并且落库的数据也无法同步。
4、微信采用用户唯一seq来实现,为每个用户分配一个发号器。seq递增。
5、当用户收到一条消息后,会请求seq发号器,生成一个递增的seq,并将其作为用户信箱的唯一递增seq。服务端保存用户已经同步到的seq位置,下次启动时只会拉取这之后的数据。

依据:微信只保留7天的未读数据。

用户信箱只保留7天,如果7天未读,则会清除消息,无法同步(信箱被清除,消息库由于不是全局有序,无法知道同步点位,所以超过7天不同步)。
7天内上线,根据上次同步点,去同步7天内的所有消息。


亲测:

新设备登陆时,是没有任何消息,说明客户端没有拉取到同步消息。
(说明,因为新设备登陆,用户也没有新消息的话,服务端维护的用户同步seq已是最大,没有消息需要同步,并且不支持消息漫游,只有本地搜索)

设备A离线,给用户发送一条消息,在设备B登陆后,B成功收到消息,重新登陆A设备,没有同步到消息。
(说明,B在同步到消息后,更新用户同步点位,A在上线后拉取不到)

设备A在线,给用户发送消息,设备B登陆后,B同步不到消息。
(说明,A在线,发送消息,用户同步到最新消息,服务端更新用户同步点位,B登陆后,根据最新点位拉取,拉取不到消息)

两台设备都在线,都能收到消息。(在线直接推送)
签名: 程序员
结论速览
1. 离线同步不再依赖“全局最大 msg_id”,而是每个会话维度的游标(session_seq 或 last_delivered_id)。  
2. 写扩散模型下,每个人的“信箱”就是一条 Timeline,用用户级单调递增的 seq 做索引即可,不用和消息 id 耦合。  
3. 读扩散模型下,客户端同样按会话维护 last_seq,拉取时把“会话维度 seq 不连续”作为缺失信号即可。  
4. 全局递增 id 的并发压力用号段批量 + 节点内自增解决;全局有序靠“号段前缀 + 节点内值”组合,不追求 1,2,3… 连续,只要单调。  

---

1. 客户端离线同步:最大消息 id 真的没用了?
场景回顾  
- 服务端只在每个会话内保证 seq 连续递增(Redis incr(sessionId))。  
- 客户端本地存了一个“已同步的最大消息 id”,但重新登录时发现这个 id 是“全局”的,和会话 seq 对不上,于是傻眼。  

解法:把游标拆到会话维度  
1. 服务端给每个会话维护 `max_seq`(Redis 单键 INCR,原子)。  
2. 客户端本地存的是一张表:  
   
```json
   last_seq_map = {
     "session_1": 127,
     "session_2":  89,
     ...
   }
   ```

3. 离线重登后,对每个会话调用

   `GET /sync?session=sid&since=127&limit=200`

   返回 ≥128 的消息列表,同时带回 本次最大 seq。  
4. 客户端把返回的 seq 更新到本地 map,下次继续从这里拉。  

好处  
- 完全不用关心“全局 msg_id”是否连续。  
- 支持多端:每台设备各自维护 last_seq,服务端无状态。  

---

2. 写扩散模型,如何维护“信箱 timeline”?
写扩散的本质

一条消息要往 N 个收件箱 各写一条索引(只写索引,不写完整消息体,消息体存一份全局库)。  

Timeline 结构设计(以单用户视角为例)  

```sql
user_timeline_1001 (
  seq          bigint,     -- 用户级递增,Redis INCR(user_1001)
  msg_id       varchar(64),-- 全局唯一,雪花或 UUID
  from_uid     bigint,
  session_id   varchar(64),
  ts           bigint,
  PRIMARY KEY(seq)
) ENGINE=InnoDB;
```

- seq 只在该用户维度递增,保证拉取时顺序连续。  
- 消息体放在单独的 msg_store(msg_id, body, …),timeline 只存引用。  
- 已读位点同样用 seq 表达:`read_seq=123`,未读数 = max_seq - read_seq。  

批量写入流程  
1. 生产者先拿到消息全局唯一 msg_id(雪花)。  
2. 对每条收件人:

   a) Redis INCR 拿到用户级 seq;

   b) 异步任务把 (seq, msg_id, …) 插入该用户 timeline 表;  
3. 在线设备收到“有新 seq”通知后,用 since=read_seq 拉取即可。  

---

3. 读扩散模型,客户端怎么知道“我同步到哪儿了”?
读扩散的特点  
- 消息只写一份在“会话库”里,用户拉取时现查。  
- 会话维度 seq 连续,但不同会话之间毫无关系。  

客户端策略:会话维度 seq + 不连续检测  
1. 本地同样维护  
   
```json
   last_seq_map = {
     "group_1":  599,
     "group_2": 1203
   }
   ```

2. 拉取接口

   `GET /group/msg?gid=g1&since=599&limit=200`

   返回 ≥600 的消息数组,以及 本次返回的最大 seq。  
3. 如果返回结果里

   `first_msg.seq != 600`

   说明 600first_msg.seq-1 之间有缺失,触发一次“补洞”重拉。  
4. 补洞后把连续的最大 seq 写回本地。  

总结

读扩散下没有“全局最大 id”也不碍事,只要会话内 seq 连续,客户端就能靠“不连续”发现缺失。  

---

4. 全局唯一消息 id:发号器压力大怎么办?
核心思路  
- 不追求 1,2,3… 连续,只要全局单调递增即可。  
- 把“全局”拆成“号段 + 节点内自增”,一次拿一批,降低 QPS。  

号段方案(百度 UID-Generator、滴滴 TinyID 同款)  
1. 数据库表  
   
```sql
   id_segment (
     biz_tag varchar(32) PRIMARY KEY,
     max_id bigint,
     step int,
     version bigint
   );
   ```

2. 发号器节点启动时,

   `UPDATE id_segment SET max_id=max_id+step, version=version+1 WHERE biz_tag='im_msg' AND version=old_version;`

   拿到 [max_id-step+1, max_id] 这一段。  
3. 内存里 AtomicLong 从 max_id-step+1 开始自增,本地无锁。  
4. 用完 80% 提前异步去再拿一段,无阻塞。  

全局有序如何保证?  
- 号段本身递增:第 1 段 1-1000,第 2 段 1001-2000 …  
- 节点内再拼一段 16/20 位自增,最终 id 结构  
  
```
  43bit 毫秒时间 + 10bit 工作机 + 11bit 段内自增 = 64bit Long
  ```

  毫秒时间保证宏观有序,工作机+段内自增保证微观不重复。  
- 即使多节点,时间戳+号段前缀也能保证全局单调。  

压测数据  
- 单节点 1 ms 可拿 1 万 id(本地自增)。  
- 10 台发号器,峰值 10 万 id/ms 无压力。  

---

一张图总结两种扩散模型的“同步游标”差异

维度        写扩散(push)        读扩散(pull)       
存储        每人一条 timeline        会话维度一份消息       
顺序键        用户级 seq(收件箱内递增)        会话级 seq(会话内递增)       
客户端游标        last_seq_map[uid]        last_seq_map[sid]       
缺失检测        服务端保证连续,客户端无需        客户端发现 seq 不连续即补洞       

---

最后的小贴士
- “seq”和“msg_id”职责分离是业界共识:

  seq 只负责“顺序+同步位点”,msg_id 只负责“去重+内容索引”。  
- 写扩散适合 读多写少、群规模小(微信单聊/小群);

  读扩散适合 写多读少、超大群(QQ 千人群、B 站弹幕)。  
- 真正落地时,推拉结合最常用:

  在线用写扩散实时推,离线用读扩散兜底拉。  

引用:sz13587236 发表于 2026-01-23 10:28
结论速览
1. 离线同步不再依赖“全局最大 msg_id”,而是每个会话维度的游标(session_seq 或 last_delive ...

写这么详细?
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部