深入理解 IM 系统

第 13 章

群资料与群成员同步

Ana 打开徒步群,每条消息要显示发的人在群里叫什么,输入 @ 要列出群里的人。手机上这份成员名单从哪来?怎么知道它变了?

这一章是初稿,会在写完前八章后再修订。

第 12 章讲的是服务端:谁在群里、每人能看哪一段。可 Ana 的手机也要一份。

打开徒步群,每条消息要显示发的人在群里叫什么;输入 @,要列出群里的人;会话列表里还要有群名、群头像。这些来自两份数据:群资料(群名、头像、公告)和成员名单(每人的群昵称、角色)。每人自己的昵称和头像是用户资料,另一回事,这一章不讲。

1. 每次打开都拉一遍

最简单的做法:打开群时,向服务端要整份成员名单。一个成员约 40 字节(用户 ID、群昵称、角色),徒步群 500 人就是 20 KB。

一天打开 20 次,就是 400 KB,是 Ana 一天收到的文字(第 11 章)的 7 倍多。服务端每次也要读 500 行。可这一天里,成员只变了几次。

省流量有两种常见做法:

  • 缓存一小时,过期才重拉。流量少了一半多,可缓存期间的变化看不到:新进群的人 @ 不到,刚改的群昵称还是旧的。
  • 带上手里那份的哈希,没变就回“没变”。名单总是新的,可只要变了一个人,就整份重发:徒步群一天变 6 次,就是 120 KB。

下面是 Ana 的一天,三个群,每次打开一根柱,柱高是这次为名单下载了多少:

徒步群500 人400 KB工作群200 人120 KB家人群12 人3.8 KB8:0012:0016:0020:0024:00

一次打开下载的(高度)用了旧名单成员变化

这一天三个群共下载 523.8 KB;每次打开名单都是最新的。

  • 每次全拉:一天 524 KB。
  • 缓存 1 小时:少了一半,可有几次打开(红圈)用的是旧名单。
  • 带哈希:只有变了之后的那次是高柱,一天 144 KB。

哈希只能说“变没变”,说不出“变了哪几个”。

2. 办法:每个群一个成员版本号

  • 服务端: 群的那一行(第 12 章取 seq 时锁的那一行)多记一个成员版本号成员版本号member version每个群一个递增的号:进群、退群、改群昵称、改角色都加一,并写在那个成员的行上。手机存着名单和它的版本号,打开群时带上,服务端没变就只回版本号,变了只回变了的行(第 13 章)。在术语表里查看。进群、退群、改群昵称、改角色,都在同一个事务里给它加一,并把新版本号写在那个成员的行上(退了的行不删,第 12 章)。
  • 手机: 存一份名单,记着它是第几版。打开群时带上版本号:“我是第 37 版。”
  • 回复: 没变,就回一个版本号,4 字节;变了,只回版本号比 37 大的那几行,每行约 44 字节。成员表上按“群 + 版本号”建个索引,读的就只是这几行。

在上面的图里选“带版本号”:同样的一天,三个群加起来半 KB 左右,每次名单都是新的。

这和第 11 章的记录版本是同一个想法:那个版本号按人记,贴在会话记录上;这个按群记,贴在成员的行上。

3. 群资料和“我加入的群”走会话记录

群名、头像、公告改了,会话列表要跟着变,不能等到打开群才知道。它们变得很少,就直接写进每个成员的会话记录,像第 11 章的置顶、免打扰一样涨记录版本,会话同步就把它带到每台设备。群里另发一条系统消息“Ana 把群名改成‘周六徒步’”,给人看。

新设备第一次登录时,群名和头像跟着会话记录一起下来(第 10 章)。

“我加入的群”也不用另一个版本号。Ana 进一个群,就多一条会话记录;退群,那条标成已退出(第 11 章);两样都涨她的记录版本。通讯录里“我的群聊”,就是会话记录里她还在的那些群。在会话列表里删掉一个群聊,只是把记录标成隐藏,人还在群里。

于是一共两个版本号,各管一摊:

版本号 按谁记 管哪些数据
记录版本(第 11 章) 按人 会话列表、我加入的群、群名头像公告
成员版本号(这一章) 按群 一个群的成员名单

进群、退群本来就是系统消息(第 12 章),群开着的时候,手机能顺手把名单改了。版本号没跟着涨,下次打开会再收到这几行,覆盖一遍,不出错。改群昵称这种不发消息的变化,等下次打开时按版本号补上。

4. 算一下

  • 一次打开: 全拉,徒步群 20 KB;带版本号,没变时 4 字节,变了一个人约 44 字节。
  • 一天: 图上三个群,全拉 524 KB,带哈希 144 KB,带版本号约 0.5 KB。
  • 服务端: 全拉每次读 500 行;带版本号,没变时只读群的那一行。

5. 代价

  • 成员表多一列、多一个索引。 每次成员变化,多改那个成员的一行。
  • 手机多存一份名单。 只存打开过的群就行。
  • 不发消息的变化有延迟。 群开着的时候改了群昵称,要到下次打开才看到;想立刻看到,就得多推一个通知。
  • 退群很久的行挪走以后, 版本太旧的手机看不出谁走了,只能整份重拉一次。

6. 其他答案

  • 每次打开全拉。 小群就这么做也行:家人群 12 人,一次 480 字节。
  • Telegram:小群带版本号,大群带哈希。 普通群的成员名单(chatParticipants)带一个群版本号 version,每条成员变化的更新(如 updateChatParticipantAdd)也带着它,客户端据此丢掉旧的,和这一章的成员版本号是一回事,只是变化是推过来的。超级群可能有几十万人,成员按页拉(channels.getParticipants),带上手里那页的哈希,没变就回 channelParticipantsNotModified;一旦变了,整页重发。
  • 只拉看得到的人。 Matrix 可以只下发当前这页消息的发送者的成员信息(lazy-loading room members),完整名单要用时再拉。适合很大的房间。
  • 每次变化推给每个成员。 名单总是新的,可一次变化要推全群,离线的人回来还是得补。
  • 超大群。 几万人的群,名单不整份下发,按需分页、搜索,留到第 40 章。

7. 这一章的决定

第 13 章:每个群一个成员版本号,进群、退群、改群昵称、改角色都加一,写在那个成员的行上。手机存一份名单和版本号,打开群时带上,服务端没变就只回版本号,变了只回变了的行。群名、头像、公告写进每人的会话记录,随会话同步到。
Ana发件箱(本地库)Ana 的笔记本同步会话记录Ben群名单+版本号消息 → ← ACK推送带 seq;回来带记录版本,只拿变了的一个程序连接层持有长连接;记着 用户 → 连接消息层(收)取 seq(消息、入群、退群),写入和待办分发层(发)待办发给区间含这个 seq 的成员业务层(旁路)群、成员、角色成员版本号存储层唯一键与会话记录发送者+消息ID会话.last_seq会话记录+版本待办:改成员记录照片分片上传(经上传服务存进对象存储)上传地址对象存储 + CDN按文件 id 存整份文件Ben 先看缩略图,点开才下载原图

群里的事,服务端和手机都清楚了。可私聊呢?陌生人能不能给 Ana 发,被她拉黑的人还能不能发,买家能不能找卖家,每个产品都不一样。下一章:谁能给谁发消息。