08.02 时间与顺序:多设备消息怎样判先后
返回第八卷 · 部分失败前置:08.01 · 会话序号前置:06.12 · Go 时间前置:01.08
08.02 时间与顺序:多设备消息怎样判先后
DeepTutor 八节初稿经技术与教学审阅后的静态课程。设备时间、事件编号、逻辑钟和向量均为纸上模型;没有运行 Go、数据库、IM 服务、时钟同步实验或站点,也不声称 OpenIM 使用下述 Lamport/向量钟。教学
m-9的身份、c-a内seq=9和显示时间各有独立职责。
一、B 收到得更晚,时间戳却更早?
延续 08.01:A 设备上的 u-a 发送 m-9,S 在会话 c-a 给它分配 seq=9,G 后来通知 B。假设两台设备已统一到同一时区,A 显示发送时刻 10:00:05,B 明明收到后才展示,却显示本机时间 09:59:58。这两个“时间”来自不同机器,各自时钟可能有偏移或以不同速度漂移;不能据较小的墙上时间断定 B 在 A 之前收到。时区换算可统一显示格式,不能修复两台钟本身不准。
先分清三个问题:①展示时间是什么人类日历时刻;②耗时是同一操作开始到结束经过多久;③事件先后能否由同进程步骤或请求/响应传递证明。用户问“为什么 B 比 A 早”,通常把①错用来回答③。数据库查询的 ORDER BY seq 又是第四种:一个会话内的业务顺序,不是设备墙钟排序。Lamport《Time, Clocks, and the Ordering of Events》
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
| 墙上时钟、单调耗时、事件与因果箭头 | Lamport 计数、向量并发、业务序号与观测边界 | 一张多设备事件图、两张逻辑钟表、一份 IM 排序决策 |
二、Go 里的一次时间值,也含两种用途
墙上时钟回答“现在几点”;它可因校时、人工修改或设备状态改变。单调时钟用于同一进程中的经过时间,目标是让本机耗时测量不被墙钟回拨扰乱。Go 官方 time 包说明 time.Now() 的 Time 可同时带墙上读数与单调读数,两个都带单调读数的 Sub/比较会用单调部分。因此,测 G 处理一次请求的本机耗时可用如下未运行示意:Go time 官方文档
start := time.Now()
// 本进程处理一次请求;教学示意,不执行
elapsed := time.Since(start)它不能替你测“A 手机开始到 B 手机结束”的精确时长:两台机器的单调刻度没有共同起点。更不能把 Go Time 经 JSON 发给 B 后拿它的“单调部分”比较;Go 的 MarshalJSON、文本/二进制序列化与格式化不保存单调读数,因为这个读数离开本进程就没有意义。睡眠或平台时钟行为也可能影响本地测时精度;这里讨论的是应选的计时口径,不是声称实际机器一定准确到纳秒。Go time Monotonic Clocks
所以日志里的 created_at、设备显示时间适合人类审计与近似定位窗口,time.Since(start) 适合某个进程内请求阶段耗时;二者都不能单独证明跨节点因果。客户端期限 100 ms 仍只是 08.01 的等待预算,不是全系统事件顺序依据。NTP 等校时协议旨在估算并调整偏移/延迟,不提供“所有设备零误差”的前提。NTPv4 RFC 5905
三、发生在前:只用本地顺序和消息传递画箭头
事件是在某个节点上发生的一次可标记动作。固定以下纸上链路:A 草拟并发送 m-9;G 收到、转发 S;S 收到后完成教学数据库提交并答 G;G 收到答复后向 B 发通知;B 收到。另有设备 C 离线编辑自己的资料 c1,没有参与这条通信。
A: a1 草稿 → a2 发送 ────────────────┐
↓
G: g1 收到 → g2 转 S ─┐
↓
S: s1 收到 → s2 提交 → s3 答 G ─┐
↓
G: g3 收到 → g4 通知 B ─┐
↓
B: b1 收到
C: c1 离线编辑(与上面各节点没有消息边)定义 “发生在前” x→y 的三条规则:同一进程中 x 在 y 之前;一次消息的发送在对应接收之前;前两类箭头可以传递。例如 a2→g1→g2→s1→s2→s3→g3→g4→b1。a2→s2 与 s2→b1 因链路可证;即使 B 的墙钟显示更早,也不能推翻。c1 与 s2 之间没有任何这类路径,二者在此模型中并发,即无法从已知因果关系判定谁影响了谁。现实世界中某个动作物理上总有先后,但没有可用通信/观察边时,系统无法据此建立业务因果。Lamport 原论文
注意我们把 S 的 s2 定义为已知提交事件。如果原请求结果未知,就不能凭 G 的“已转发”日志画出 s2;要查询权威状态或保留未证实的虚线。这把 08.01 的观察限制带进事件图。
四、Lamport 逻辑钟:能保住因果箭头,不能倒推出箭头
给每个节点一个从 0 开始的整数。发生本地事件或发送消息时计数加 1;接收附带发送方计数 v 的消息时,接收方计算 max(本地计数,v)+1。下面严格按第三节的事件流手算,不是数据库、OpenIM 或 Go runtime 的真实计数:
| 节点 | 事件 | 计算 | Lamport 值 |
|---|---|---|---|
| A | a1 草稿 | 0+1 | 1 |
| A | a2 发送给 G | 1+1,携带 2 | 2 |
| G | g1 收 A | max(0,2)+1 | 3 |
| G | g2 发给 S | 3+1,携带 4 | 4 |
| S | s1 收 G | max(0,4)+1 | 5 |
| S | s2 提交 | 5+1 | 6 |
| S | s3 答 G | 6+1,携带 7 | 7 |
| G | g3 收 S | max(4,7)+1 | 8 |
| G | g4 通知 B | 8+1,携带 9 | 9 |
| B | b1 收 G | max(0,9)+1 | 10 |
| C | c1 独立编辑 | 0+1 | 1 |
由构造可知:若 x→y,则 L(x)<L(y)。但反过来不成立:L(c1)=1 < L(s2)=6,在固定事件图里 c1 与 s2 仍并发。数字只排出一种与因果兼容的候选顺序,不能凭小于号补画没有发生的消息边。若用 (L,节点 ID) 打破并列,可以人为给并发事件排一个总顺序,那只是明确的决策规则,不会使 c1 真正导致 s2。Lamport 原论文
不要把 L(s2)=6 写进 messages.seq:seq=9 是 S 在 c-a 的业务事务里分配的序号;Lamport 值在这个纸上事件图里只为解释因果。逻辑钟会随参与节点和事件定义改变,不能取代用户可见历史的持久排序合同。
五、向量钟:两个“谁也没收到谁”的事件怎样被识别
Lamport 标量能保证因果时数值递增,却不能仅凭 1<6 判断因果。为看见并发,再画一个只有 A、C 两个进程的小模型,与上面的 G/S/B 十事件表分开。这里 A 的 vA1 本身就是发往 C 的事件,C 的 vC1 在收到它之前独立发生。每个进程保存 [A 计数,C 计数],本地事件增加自己的分量,接收他人的向量时逐分量取最大,再增加自己的分量:
| 事件 | A 当前向量 | C 当前向量 | 推论 |
|---|---|---|---|
| 初始 | [0,0] | [0,0] | 两边都未发生本题事件 |
| A 发出事件 vA1 | [1,0] | [0,0] | A 有一个发送事件 |
| C 独立事件 vC1 | [1,0] | [0,1] | [1,0] 与 [0,1] 各有一维更大,并发 |
| C 后来收到 A 的 vA1 | [1,0] | max([0,1],[1,0])=[1,1],C 再增为 [1,2] | vA1、vC1 都发生在这次接收之前;它们仍相互并发 |
比较规则:若向量 X 的每一分量都不大于 Y,且至少一维更小,X 可排在 Y 的因果之前;若 X、Y 各有一维大于对方,则不可比较,表示本模型中的并发。这里 [1,0] 与 [1,2] 可比较,而 [1,0] 与 [0,1] 不可比较。Mattern 向量时间原论文
向量为每个参与进程保留一个分量,参与者增加时元数据也可能增长;真实多设备 IM 不会因为本章画了向量就自动使用向量钟。它是帮助理解“并发更新需冲突政策”的工具,后续 08.04 才联系一致性与可见承诺。
六、四种“顺序”怎样落到消息产品里
同一条 m-9 至少带四种不同信息:message_id 是稳定身份,用于查重与提交未知时对账;seq=9 是 S 在 c-a 内分配的业务顺序,供历史 ORDER BY seq DESC 与游标使用;created_at 或设备时间适合展示/审计,但受时钟偏移、精度和采样位置影响;逻辑钟是本章解释事件因果的教学模型。它们不能互相推算。06.12 数据库业务案例
| 业务问法 | 应先看什么 | 不能只凭什么 |
|---|---|---|
c-a 最新三条如何排 | 权威写入分配的会话 seq,本题 9、8、7 | 客户端墙上时间 |
A 是否在 B 看见前发送了 m-9 | 请求/通知的发送→接收因果链 | B 显示 09:59:58 比 A 的 10:00:05 小 |
| 两设备各自离线改了资料,谁覆盖谁 | 版本/冲突政策与可观测因果 | 单个 Lamport 数值小于号 |
m-9 请求超时是否已保存 | 权威 message_id 状态与授权核验 | 时间戳、客户端动画或设备通知 |
c-a.seq=9 与 c-b.seq=9 可以同时存在;会话内唯一不产生所有会话的全局先后。多端已读还要定义“哪个设备在何时对哪条已知消息确认”,不能用 B 的设备时间小于 A 的时间就回滚读回执。排序键若不唯一,06.02/06.05 已要求附稳定次级键;若业务选择真正的全局顺序,要另有全局权威分配与代价,不能把客户端时间戳当免费全局序列号。
七、观测与故障:时钟信息怎样用、怎样不用
记录事件时,保存节点、业务 message_id、请求/Trace ID、发生的阶段、对应发送/接收边、已知的权威 seq,并附墙上时间与本节点单调耗时。多节点日志可以用墙上时间找一个近似窗口,但要标注时钟来源和误差,不应只把日志按时间戳排序就宣称因果链已证明。若一次请求经历超时重试,两个请求尝试的时刻可能交错;稳定 ID 与权威提交记录仍是核心。Go time 官方文档
校时可以减少偏差,不能令所有设备钟完全相同。若 A 的墙钟回拨,本机用两次墙钟字符串相减甚至可能得到负“耗时”;进程内 time.Since(start) 更适合测阶段持续时间。跨进程要用明确的消息边、可核对的提交/复制位置、请求关联 ID 和定义好的时间误差范围;不要把另一个进程反序列化出来的 Go Time 当作共享单调钟。闹钟/定时器、数据过期与租约在后续 08.07 还要引入故障模型,不能仅靠“现在本机几点”保证全局归属。
八、交付一张不会被时间戳骗倒的事件图
交付 A/G/S/B 的事件图和上面的 1–10 逻辑钟表;另用 A/C 的二维向量说明并发,再写一段业务说明:历史按 c-a.seq 排,设备时间用于显示,客户端超时按 message_id 查权威。每个箭头必须来自同进程顺序或真实发送→接收证据;若只是墙钟大小,标成未证实。
分层练习与反馈
1–8 先练时间词汇,9–16 推导因果与逻辑钟,17–22 处理 IM 排序与观测。先预测,再展开答案。
1. A 显示 10:00:05、B 显示 09:59:58,能证明 B 先收吗?
不能。两个设备的墙钟可能有偏移/漂移;应看通知的发送→接收链。
2. 时区统一成 UTC 能消除设备时钟偏差吗?
不能。时区只改变同一时刻的表示;设备钟本身可能不准。
3. Go 中测一个处理函数本机耗时,`time.Since(start)` 主要依靠什么?
两个时间值都带单调读数时,Go 优先用单调部分作差;它不是跨节点时钟。
4. `time.Time` 经 JSON 发给 B 后还带原进程单调读数吗?
不带。序列化会省去它,因为单调读数离开原进程没有意义。
5. `message_id` 和 `seq` 分别解决什么?
前者是稳定消息身份/查重与对账键;后者是在特定会话内的业务排序键。
6. `created_at` 能自动给所有会话消息一个无冲突总序吗?
不能。时钟偏差、精度并列和产生时间位置都使它不足以单独承担该合同。
7. 发生在前的三条生成规则是什么?
同进程内顺序、消息发送先于对应接收,以及关系的传递闭包。
8. `c-a.seq=9` 与 `c-b.seq=9` 是否冲突?
不冲突;本模型只要求同一会话内唯一,跨会话不能凭 seq 比先后。
9. a2 发送到 G 与 g1 接收,谁发生在前?
a2→g1,这是同一条消息的发送→接收边。
10. a2 与 s2 之间能画因果箭头吗?
能。a2→g1→g2→s1→s2,由传递性得到 a2→s2。
11. c1 与 s2 为什么在本图中并发?
C 离线独立编辑,没有到 S 或来自 S 的消息边;两事件之间不存在已知因果路径。
12. g1 收到携带 2 的消息,G 本地原为 0,Lamport 值是多少?
max(0,2)+1=3。
13. S 在 s1 收到携带 4 的消息,本地原为 0,s1 与 s2 值分别多少?
接收 s1=max(0,4)+1=5,后续本地提交事件 s2=6。
14. G 发出时值 4,收到 S 带 7 的答复,g3 值多少?
max(4,7)+1=8;随后 g4 发 B 的值为 9。
15. `L(c1)=1<L(s2)=6` 能证明 c1 导致 s2 吗?
不能。若有因果箭头则逻辑值递增;逻辑值较小的逆命题不成立。
16. 给两个并发事件按 `(L,nodeID)` 排序后,它们变成因果相关了吗?
没有。这只是人为选定可重复的总序,不改变过去的通信历史。
17. `[1,0]` 与 `[0,1]` 的向量关系是什么?
各有一维大于对方,不可比较,表示本题 A/C 独立事件并发。
18. C 原为 `[0,1]`,收到 A 的 `[1,0]` 后再记接收事件,得到什么?
先逐维取最大 [1,1],再加 C 分量得到 [1,2]。
19. C 接收 A 后,先前 vA1 与 vC1 就不再并发了吗?
仍并发。后来的接收事件在二者之后,不会改写这两个早期事件的关系。
20. 本章 Lamport `s2=6` 能直接作为 `c-a` 的下一个消息 seq 吗?
不能。业务序号由权威消息写入规则分配,本题已是 seq=9;两套数字职责不同。
21. 两台机器日志墙钟顺序相反,排查时先找什么?
找同一消息/请求关联 ID、每跳发送→接收与权威提交证据,再结合墙钟误差做近似窗口。
22. A 超时后只凭 `created_at` 判断 `m-9` 已存,够吗?
不够。按稳定 message_id 经授权查询权威状态;设备显示时间或客户端时间不证明数据库提交。
本章完成标准与下一步
不看答案时,能手算 a1–b1 的 1–10 逻辑值、指出 c1 与 s2 并发,比较 [1,0] 与 [0,1],并说明 message_id、seq、墙上时间与进程内耗时各负责什么,才算完成第一轮。第二轮把真实多设备事件关联到有证据的发送/接收边,并测量本地耗时;本章没有代替运行。
按学习路线,下一章08.03 复制的目标与代价比较复制确认点和滞后,08.04 再把“发送后读到什么”写成一致性承诺;完成这两章后进入第七卷。