什么是单调读?如何避免读到旧版本?
简化版
单调读保证同一会话后续读取不会返回比该会话已经见过的版本更旧的数据。 它不要求读到全局最新,也不等于读己之写。 常见实现是会话保存 lastSeenVersion,读请求携带最低版本;副本不足时等待、转到更进度节点或返回重试。
详细版
多副本负载均衡可能先读到版本 20,下一次切到落后副本只读到 18。 客户端、网关或服务端会话保存每个对象/分区的版本水位。 副本仅在 appliedVersion 不小于要求时响应。
粘性路由是简单优化,但原节点故障仍需版本 token。 token 必须带分片与作用域,不能拿一个分区的 offset 比另一个分区。 无登录、多设备或 token 丢失时,保证范围会缩小,需要在接口契约中说明。
完整版教学
一、单调读的语义
会话第一次看见 v12,后续不能看到 v10。 它允许后续仍看到 v12,即使全局已有 v15。 因此它是会话顺序保证,不是线性一致。
单调读防止“时间倒流”,不承诺“永远最新”。
二、为何会倒退
副本 A 已应用到 120,副本 B 只到 115。 第一次请求路由 A,第二次因负载均衡路由 B。
read(A) -> version 120
read(B) -> version 115 // violates monotonic read
缓存回填、故障切换和跨地域路由也会造成倒退。
三、版本令牌
响应返回 version/offset/commit token。 客户端在后续请求携带 minVersion。 服务端选择进度足够的副本,或等待本地追平。 token 要防篡改并限制大小,不能泄露内部敏感拓扑。
四、处理不足版本
| 策略 | 优点 | 代价 |
|---|---|---|
| 等待本地追平 | 路由稳定 | 增加尾延迟 |
| 转主/新副本 | 快速满足 | 增加主压力 |
| 返回 retry | 保护容量 | 客户端复杂 |
| 返回旧值 | 可用性高 | 破坏承诺 |
选择应受 deadline 控制,不能无限等待。
五、粘性会话
同一用户固定到同一副本可减少倒退。 但副本重启、扩缩容或热点会打破粘性。 因此粘性只能优化,版本令牌才是可验证约束。 路由键还要避免用户隐私直接暴露。
六、与读己之写区别
读己之写要求会话完成写 v13 后,读至少见 v13。 单调读只约束已读版本不倒退,未必知道自己的写。 两者可共用 minVersion token,但 token 更新来源不同。 面试回答要分清这两个会话保证。
七、观测与边界
监控版本不足等待、转主比例、超时和发生倒退的检测数。 token 按实体、分区或全局版本设计,粒度越细元数据越大。 跨设备若不共享 token,两个会话各自单调但相互不保证。 删除与墓碑也要有版本,否则旧副本可能“复活”数据。
八、常见误区与追问
- 误区:单调读就是强一致读。 它不保证全局最新。
- 误区:粘性路由绝对可靠。 故障切换会破坏粘性。
- 误区:时间戳可直接当版本。 时钟回拨会破坏单调性。
- 误区:全局一个 token 适合所有分片。 版本必须有可比较作用域。
- 追问:副本未追平怎么办? 等待、转新副本或明确重试。
- 追问:与读己之写有何不同? 一个约束已读历史,一个约束自己的已完成写。
- 追问:删除如何处理? 用带版本墓碑防旧副本复活。
九、加强记忆
- 单调读是不读到比已见版本更旧。
- 不承诺全局最新。
- lastSeenVersion 随请求传播。
- 副本不足时等待、转路由或重试。
- 粘性是优化,版本 token 才是约束。