# 第 11 章：会话同步

> 会话在服务端，手机去同步。Ana 有 2,000 个会话，每次打开都要下载 2,000 个最新 seq，其中只有十来个变了。怎么只把变了的发下去？

深入理解 IM 系统 · https://im.liko.page/zh/conversation-sync/

[第 10 章](/zh/fan-out/)定了：会话存在服务端，手机照抄。可同步还是第 6 章那样：每次回来，问一遍每个会话的最新 seq（会话里最新一条消息的序号），和本地比，大的就是有新消息。

Ana 有 2,000 个会话，每次就下载 2,000 个 seq，连会话 ID 每个 12 字节，共 **24 KB**。一天打开 20 次是 480 KB，是她一天收到的文字（267 条，约 53 KB）的 9 倍。可两次打开之间，变了的会话只有十几个。服务端也白干：每次都要读 2,000 个 seq。

服务端全发，是因为它不知道这台手机上次拿到了哪。这一章让手机只拿变了的。

## 1. 想法：给每个会话的那一行贴版本号

这一章有两个数，先分清：

- **seq**：每个会话自己的消息序号。徒步群第 4,823 条消息，seq 就是 4,823，群里人人一样。
- **版本号**：每个人自己的计数器（第 6 章的记录版本）。Ana 的哪个会话变了，她的版本号就加一。

*（互动图：请在网页上查看 https://im.liko.page/zh/conversation-sync/）*

1. 服务端 Ana 的每个会话一行（会话记录），多记一个最新 seq。
2. 每来一条消息，给这个会话的每个成员改一下自己的那一行：写上新 seq，贴上这个人的新版本号。
3. 手机记着上次拿到的最大版本号，回来只问“比它大的行给我”。

图里 Ana 的手机上次同步到版本 9,000。徒步群（9,013）和工作群（9,004）都比它大，发回来；读书群（8,571）没变，不发。

下面是 Ana 的一天，每格是她的一个会话：

*（互动图：请在网页上查看 https://im.liko.page/zh/conversation-sync/）*

- **每次全发**：每次 24 KB，2,000 格全亮，真正变了的只有十来格。
- **只拿版本号更大的**：每次三四百字节。回复丢了（标着“丢”的那次），手机的版本号就不动，下一次照样问，那几个会话再发一遍（图上橙框的格子），一个不漏。

## 2. 扩散又回来了一点

第 10 章说，服务端建，消息不用给成员写任何东西。这一章又写回来了：每条消息，给每个成员改一行。第 6 章“其他答案”里列过这个做法，当时会话不多，不值得。

和信箱比，拿 500 人的徒步群算：

- **写的次数一样。** 信箱追加 500 条，这里改 500 行；每次改还更贵一点，要锁这个人、涨版本号。
- **行数不涨。** 信箱一天追加 50 万条，越攒越多；这里改来改去还是那 500 行。热闹时，同一行的好几次改动能合成一次。
- **读省了。** 手机只拿变了的十几行，服务端也只读这十几行。

## 3. 两件要做对的事

**版本号不能先领后写。** 要是先领号、再写那一行，就会漏：

1. 消息甲给 Ana 领到 101，还没写完；
2. 消息乙领到 102，先写完了；
3. 手机这时来同步，拿到 102，记下“到 102 了”；
4. 甲这才写进去，101 比手机记下的小，以后不会再发。

办法：取号和写那一行放在同一个事务里，取号时锁住这个人（`UPDATE users SET record_version = record_version + 1`，第 6 章就这么取号）。前一个没写完，后一个拿不到下一个号，**版本号小的一定先写完**。服务端按版本号从小到大回，手机记回复里最大的那个，不另去问服务端“现在到几号了”。

**发消息的人不用等。** 徒步群一条消息要改 500 行，不能让发消息的人等着。发消息时，同一个事务里只记一行待办：“徒步群 4,823，改所有成员的会话”；回 ACK、推送照常。之后分发层按待办，一人一个小事务慢慢改，挂了重启接着改。改两次也没关系：seq 只往大里改，版本号多涨一次，不过让这个会话多发一次。

退群、删会话也一样：改这一行（标成删除，不真删）并涨版本号，手机照样收到。

## 4. 算一下

- **手机：** 每个变了的会话一行 28 字节（会话记录 24 字节，加最新 seq 4 字节），十几个约 **360 字节**一次，一天约 7 KB。
- **服务端：** v2 高峰每秒 139 条消息，平均每条 6.67 个收件人（全书的数），约 **927 次**改行，约占一台存储写入能力的一成。

## 5. 代价

- **写跟着群的人数涨。** 500 人的群每秒说 10 条，就是每秒 5,000 次改行，一台存储的一半。
- **会话那一行比消息晚一点。** 待办没改完时来同步，看不到最新那条；推送会先到，不然就等下一次同步。
- **每人取号的那一行是热点。** 一个人在好几个热闹的群里，这一行每秒被锁好几次，谁在它上面开个长事务，他的会话更新全得排队。

## 6. 其他答案

- **用更新时间当版本号。** 会话那一行记一个 updated_at，手机拿本地最大的那个往后一页页拉，直到拉完，不用另外的计数器，也不用锁每个人。小心两处：同一时刻会有好几行，翻页要带上会话 ID 一起排；时间是写的时候取的，不等于提交的先后，早取时间、晚提交的那一行会落到手机已经翻过去的地方，和第 3 节是同一个洞。常见的补法是每次往回多拉几秒；比这更长的事务或时钟偏差仍会漏，还得有个兜底的全量对账。
- **服务端替每个连接记一份。** Matrix 的简化同步（[MSC4186](https://github.com/matrix-org/matrix-spec-proposals/blob/erikj/sss/proposals/4186-simplified-sliding-sync.md)）这样做：记住上次给这个连接发过什么，下次只发变了的。消息来了不用改成员的行；代价是每个连接一份状态（v2 约 360 MB），还要确认上次的回复真的到了。
- **大群不改，回来时扫。** 小群走这一章的做法，大群像第 6 章那样问最新 seq。两条路要一起维护。
- **每次全发。** 第 6 章的做法。会话不多的人（平均 200 个，2.4 KB）其实不疼。

## 7. 这一章的决定

*（互动图：请在网页上查看 https://im.liko.page/zh/conversation-sync/）*

**决策卡**

- 问题：会话很多的人，每次同步都下载所有会话的最新 seq：Ana 有 2,000 个会话，一次 24 KB，几乎全没变。
- 选择：每来一条消息，给每个成员的会话那一行写上最新 seq、贴上这个人的新版本号（取号和写入在同一事务里，锁住这个人）；发消息时只记一行待办，之后再改。手机记上次的最大版本号，只拿比它大的行。
- 代价：每条消息又要给每个成员写一次（v2 高峰每秒约 927 次）；会话那一行比消息晚一点；每人取号的那一行是热点。
- 什么时候重新考虑：待办按哪一刻的成员名单（第 12 章）；已读也走这个版本号（第 15 章）；待办换成消息日志（第 29 章）；成员上万的群不走这条路（第 40 章）。
- 其他答案：用更新时间当版本号；服务端替每个连接记一份（Matrix MSC4186）；大群不改、回来时扫；每次全发。

会话同步只拿变了的。可“群里有谁”到现在还是一张随手查的名单：Ben 退了群，下一条消息还推给了他；新进来的人，看得到他进群以前的消息。下一章：入群与退群。
