bigywater.tech
分布式事务 · 26

读己之写、单调读:会话一致性怎么实现

最终一致的读,用户也会碰到“我刚改的怎么没了”“怎么刷新一下又变旧了”“先看到回复后看到帖子”这些怪事。读己之写、单调读、因果一致这三个会话保证怎么实现?各自要付出什么?这篇用一主两从的 Redis 和客户端自己的实现,把每种异常和对策量出来。

184 → 0
读己之写:随机读 replica 读到旧值的次数(共 1500 次读)→ 偏移量令牌法
99 → 0
单调读:随机选 replica 时读到的值倒退的次数 → 固定 replica 或记最大值
57 → 0
因果:看到回复却读不到依赖的帖子的次数(共 300 次)→ 检查依赖版本

读己之写

实测 · 本机执行输出
  读法                                                 | 读到不是自己刚写的值(旧值) | 由 replica 服务的读占比 | 单次读平均耗时
  ---------------------------------------------------+----------------+------------------+--------
  随机选一个 replica 读                                    | 184 / 1500     | 100%             | 0.30 ms
  写后 50 ms 内读 master,之后读 replica                     | 0 / 1500       | 40%              | 0.33 ms
  偏移量令牌:replica 的偏移量 ≥ 写入时 master 的偏移量才读它,否则读 master | 0 / 1500       | 90%              | 0.49 ms
  随机选 replica:慢 replica(R2)落后约 30 ms,写后 0、20 ms 时读它必然是旧的,读 R1 也可能旧;“写后 50 ms 内读 master”要求复制延迟小于 50 ms,否则仍会读到旧值(R2 延迟 30 ms,所以这里恰好够);令牌法不依赖延迟上界,代价是每次读多问一次 replica 的偏移量

写完在一段时间里读 master,或带上写入时的偏移量令牌、只读已追上的 replica;前者依赖延迟上界,后者多问一次偏移量。

单调读

实测 · 本机执行输出
  读法                          | 读到的值比上一次小(倒退)的次数 | 读到的值平均比 master 当前值落后多少 | 改读 master 的次数
  ----------------------------+------------------+------------------------+--------------
  每次随机选一个 replica             | 99               | 2.9                    | 0            
  会话固定读同一个 replica(这次随机抽到 R2) | 0                | 5.9                    | 0            
  记住读到过的最大值,读到更旧的就改读 master   | 0                | 0.2                    | 189          
  随机选 replica 会让会话在快慢两个 replica 之间来回跳,值倒退;固定到一个 replica 就不会倒退(同一个 replica 内部是有序的),但固定到慢的那个会一直更旧,而且这个 replica 挂了要换;“记住最大值”的做法不倒退,代价是读到旧值时要多读一次 master

随机在快慢 replica 之间换会让值倒退;固定一个 replica 保顺序但可能一直更旧;记住读到的最大值、更旧时改读 master,兼顾顺序与新鲜度。

因果一致

实测 · 本机执行输出
  读法                     | 看到回复却读不到它依赖的帖子的次数 | 等待依赖的次数 | Carol 读两个值平均耗时
  -----------------------+-------------------+---------+---------------
  不检查依赖:读到回复就直接去读帖子      | 57 / 300          | 0       | 0.54 ms       
  检查依赖:读到回复后,帖子的版本不够就等到够 | 0 / 300           | 65      | 2.77 ms       
  两个分片各自复制、延迟不同,互相之间没有顺序;因果关系要靠回复里带的依赖版本(这里是帖子号)在读的时候检查,等不到才继续读,代价是在慢 replica 上等

两个分片各自复制,先写的帖子可能比后写的回复晚到;回复里带上依赖的帖子版本,读的时候检查,不够就等。

分步演示

会话一致性
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.7.0 与 PostgreSQL 16.15(Homebrew,每个实验自己 initdb、自己起实例,端口 9150 以上,只监听 127.0.0.1)/ Python 3.14.8 + PyMySQL 1.2.3 + psycopg 3.3.6 / Redis 8.10.2 / Kafka 4.3.1 / RabbitMQ 4.3.6 / etcd 3.7.2(按各篇注明用到哪些);不连任何外部服务,不碰本机已有的数据库实例;故障用 kill -9 进程、让连接断开或在代码里的指定位置抛异常来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用自己起的 Redis 8.10.2:分片 A 一主两从(R2 经过我加的延迟代理,每个方向 +15 ms)、分片 B 一主一从;复制是异步的;三个会话保证都是我在客户端(Python + redis-py)里自己实现的,不是 Redis 的功能;偏移量取自 INFO replication 的 master_repl_offset / slave_repl_offset;时间间隔用 sleep 控制,数字会随运行略有不同
    • 没有验证:MongoDB 的因果一致性会话(afterClusterTime)等产品自带的实现;Redis Cluster / Sentinel 故障转移时令牌失效的情形;令牌怎么在多个应用服务器之间传递(这里是同一个进程里的一个变量);会话保证与多 key 事务的关系;“写后 50 ms 内读 master”的 50 ms 是我按 R2 延迟约 30 ms 挑的,真实系统里的延迟上界通常不知道
    京ICP备2026067502号-1京公网安备11010502063487号