MongoDB 读写策略与一致性取舍:readConcern、readPreference 与 causal session

MongoDB 读写策略与一致性取舍:readConcern、readPreference 与 causal session

先看一个非常常见的线上现象。用户在页面点"下单",接口返回成功,页面立刻跳转到订单详情,结果提示"订单不存在";刷新两秒后又出现了。开发同学第一反应往往是"MongoDB 丢数据了",于是去查 writeConcern,发现已经是 majority;再查日志,写入确实成功,只是详情页读到的是一台还没同步到这条数据的从节点。问题不在写,而在读与写的路径没有对齐。

MongoDB 的副本集默认让写落在主节点、让读可以落到从节点,这带来了吞吐和就近访问的好处,也带来了"同一份数据在不同节点上处于不同时间点"的常态。要回答"为什么刚写完查不到""切主后数据会不会回滚""跨机房读会不会读到旧值",就必须把三组旋钮分清楚:写方承诺到什么程度(writeConcern)、读方要求看到什么程度的数据(readConcern)、读方允许去哪个节点拿数据(readPreference),以及会话如何把一次因果相关的读写串起来(causal session)。

可以先记住一个最小模型:writeConcern 决定"写入在多少个节点上落定才算成功",readConcern 决定"我读的时候允许多新的数据被看到",readPreference 决定"这次读走主节点还是从节点"。三者互相独立,但共同决定一次请求最终看到的世界。把它们理解成三张独立的表,比记成一组"一致性级别"更容易迁移到真实故障里。

1. 从一次"刚写完读不到"说起

假设一个三节点副本集:主节点 P 在机房 A,从节点 S1 在机房 A,从节点 S2 在机房 B。业务写入使用默认的 w:1,读使用默认的 readPreference: primaryPreferred 之外的 secondaryPreferred,并且客户端配置了就近读取。此时一条订单写入在 P 上确认后立刻返回成功,S1 和 S2 通过 oplog 异步复制。如果详情页的读被路由到 S2,而 S2 的复制位点还落后,就会读到"不存在"。

这个场景里有三个独立变量:

  • 写入在几个节点确认:w:1 只保证主节点写成功,不保证从节点已收到。
  • 读取允许多旧的数据:默认 readConcern: local 只要求读到本节点当前可见的数据,不判断它是否已被多数派确认。
  • 读取去哪个节点:secondaryPreferred 会把读压力分散到从节点,代价是可能读到落后的副本。

看起来相同,但区别在于:writeConcern 是"写方对持久性和复制范围的承诺",readConcern 是"读方对数据新鲜度的要求",readPreference 是"读方的路由偏好"。三者的组合才是真正的一致性等级,单独提高某一个往往解决不了问题。

下面这张表先把三者的职责钉死,后续所有讨论都围绕它展开。

旋钮 回答的问题 作用位置 默认值 典型代价
writeConcern 写到几个节点才算成功 写请求 w:1 提高 w 增加写延迟与阻塞风险
readConcern 读到的数据要达到什么确认程度 读请求 local 提高级别增加读延迟、可能等待
readPreference 这次读去哪个节点 读请求 primary 走从节点降低主压但可能读到旧值

2. 副本集里一次读写到底走了哪条路

要讲清取舍,先建立一张全局链路图。MongoDB 副本集由一个主节点和若干从节点组成,客户端通过驱动连接,驱动维护对各节点的连接池和拓扑视图。写只能发往主节点;读根据 readPreference 可能发往主节点或从节点。

text 复制代码
客户端(驱动)
   |
   | 1. 拓扑发现: hello/isMaster -> 得到 P/S1/S2 角色与延迟
   v
+----------------+        2. 写请求(仅主)        +----------------+
|  主节点 P      | <---------------------------- |  应用/驱动     |
|  apply oplog   |                               +----------------+
+----------------+                                      |
   |  3. 异步复制 oplog                                  | 4. 读请求(按 readPreference)
   v                                                     v
+----------------+   oplog   +----------------+   +----------------+
|  从节点 S1     | <--------- |  从节点 S2     |   | 读到 P/S1/S2   |
+----------------+            +----------------+   +----------------+
   |                               |
   | 5. 各自按 readConcern 判断可见性 |
   v                               v
  返回结果(可能处于不同复制位点)

这张图要帮助理解的是"读和写是两条独立路径":写路径只经过主节点,复制是异步的;读路径由驱动按偏好选择节点,节点只对自己已应用的数据负责。它不能替代的部分是:真实网络里还存在心跳选举、连接池排队、写关注等待复制确认等过程,这些会在后面章节展开。

一次请求按时间顺序大致是:驱动先通过 hello 命令获得拓扑,知道谁是主、各从节点的复制延迟;然后把写发往主节点,主节点写本地 oplog 并按 writeConcern 决定何时返回;读请求根据 readPreference 选择节点,节点根据 readConcern 决定读到哪个时间点的数据;如果是 causal session,驱动还会在请求上携带 afterClusterTime 和 operationTime,让本次读至少等到上次写对应的复制位点。

3. writeConcern:写入要落到几个节点才算数

writeConcern 是写方对"写成功"的定义。w:1 表示主节点本地写入成功即返回,w:majority 表示要等到多数派节点确认(三节点副本集是 2 个),w:0 表示不等待任何确认就返回。w 越大,单次写的延迟和失败概率越高,但主节点故障切走后数据被回滚的概率越低。

javascript 复制代码
// 1) w:1 写订单:主节点本地确认即返回,最快,但切主可能回滚
use shop
db.orders.insertOne(
  { orderId: "O1001", userId: "U9", amount: 199.00, status: "CREATED" },
  { writeConcern: { w: 1 } }
)

// 2) w:majority 写订单:多数派确认后才返回,切主后一般不会回滚
db.orders.insertOne(
  { orderId: "O1002", userId: "U9", amount: 299.00, status: "CREATED" },
  { writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
)

两个例子的差别在返回值语义上:w:1 返回时,S1、S2 可能还没收到这条 oplog;w:majority 返回时,至少两个节点已持久化(若 j:true 还要求 journal)。如果 wtimeout 到期仍未满足,写入本身可能已生效但驱动报超时,业务必须把这视为"结果未知",而不是简单重试插入,否则会产生重复订单。

什么时候用 w:majority?当这条写丢了要付出真实业务代价时,例如支付、订单状态流转、账户余额变更。什么时候可以退到 w:1?当日志、埋点、可重放的消息缓存这类数据可以容忍少量丢失时。边界在于:w:majority 不是"绝对不丢",它在极端分区下会让写失败而非丢数据,吞吐会明显下降,且需要与 readConcern: majority 配合才能避免读到尚未多数派确认的数据。

writeConcern 确认范围 切主后回滚风险 写延迟 适用场景
w:0 不等待 高 最低 可丢的埋点
w:1 主节点 中 低 普通业务、可补偿
w:majority 多数派 低 中高 订单、资金、状态机
w:majority + j:true 多数派且落 journal 最低 高 强持久化要求

4. readConcern:读的时候允许多新的数据被看到

readConcern 是读方对"我看到的这份数据需要达到什么确认程度"的要求。常见级别有 local、available、majority、linearizable、snapshot。它不改变读去哪个节点,只改变节点返回数据前的判断条件。

javascript 复制代码
// A) local:读本节点当前已有数据,速度最快,可能读到尚未多数派确认的数据
use shop
db.orders.find(
  { orderId: "O1002" }
).readConcern("local")

// B) majority:只返回已被多数派确认的数据,可避免读到随后被回滚的数据
db.orders.find(
  { orderId: "O1002" }
).readConcern("majority")

// C) linearizable:线性一致读,必须走主节点,会等待并发写确认,代价最高
db.orders.find(
  { orderId: "O1002" }
).readConcern("linearizable")

local 与 majority 的差别,在"主节点写入成功但尚未复制到多数派,主节点随后宕机"时会暴露出来:local 可能先让你看到这条记录,之后这条记录随主节点回滚而消失;majority 则要求数据在多数派上已确认,因此不会出现已读又被回滚的情况。注意 majority 并不等于"读到最新的多数派数据",它只保证你读到的是多数派已经看到的那部分,可能仍然落后于主节点当前最新写入。

再看一个容易混淆的点:linearizable 是读级别的强一致,只对单文档读生效,必须走主节点,并且要等待所有在进行中的多数派写完成;它不是"把整个集合锁住"。snapshot 则常用于多文档事务,让事务内所有读看到同一个数据快照。选择时按业务问一句:这条数据如果读到后又被回滚,业务能否容忍?不能容忍就用 majority 起步;只有在必须读到"此刻最新的多数派确认值"时才用 linearizable。

5. readPreference:这次读去主节点还是从节点

readPreference 决定读请求的候选节点集合。默认 primary 是全部走主节点,读写都在主节点;secondaryPreferred 优先从节点,从节点不可用再回主;nearest 按网络延迟就近选择;primaryPreferred 优先主节点,主不可用才读从。它还配套 maxStalenessSeconds 限制从节点最多落后多久。

javascript 复制代码
// 优先读从节点,允许最多落后 90 秒;超过就退回主节点或报错
db.orders.find(
  { userId: "U9" }
).readPref("secondaryPreferred", [{ maxStalenessSeconds: 90 }])

// 按标签选择机房:只读机房 A 的从节点
db.orders.find(
  { userId: "U9" }
).readPref("secondary", [{ region: "A" }])

maxStalenessSeconds 的作用是把"读旧值的容忍度"变成一个可度量的数:驱动会结合从节点上次写时间与心跳间隔估算其陈旧程度,超过阈值的从节点不被选为候选。它降低读到严重滞后数据的概率,但不等于实时同步------90 秒意味着最坏情况下你仍可能读到一分钟前以上的状态,支付详情页就不该给这样的容忍度。

读写分离最容易踩的坑是"本进程刚写完马上跨进程读"。写走主节点,读走从节点,两者之间没有任何顺序保证。解决办法有两个方向:给这条读加 readConcern: majority 并配合 causal session 让它至少等到写对应的位点;或者在这段业务里直接 readPreference: primary。前者保留从节点读的吞吐优势,后者最简单但会把读压力收回主节点。

6. 因果一致性:让"我刚写的,我必须能读到"成立

因果一致性解决的是"同一客户端会话内,写后读、读后写必须看到前一步的效果"。MongoDB 用会话(session)实现:一次写成功后,服务端返回 operationTime;客户端在同一会话内发下一次读时,驱动会带上 afterClusterTime,要求服务端至少等到那个时间点对应的数据都可见。它不需要全局锁,也不需要让所有读都走主节点。

javascript 复制代码
// causal consistency 需要显式开启 causallyConsistent=true
const session = db.getMongo().startSession({ causallyConsistent: true });
const coll = session.getDatabase("shop").orders;

// 1) 写入并记住该会话的操作时间
coll.insertOne(
  { orderId: "O2001", userId: "U9", amount: 88.00, status: "PAID" },
  { writeConcern: { w: "majority" } }
);

// 2) 同一会话内读:驱动自动携带 afterClusterTime,保证能看到上一步写
const doc = coll.findOne({ orderId: "O2001" }, { readConcern: { level: "majority" } });
printjson(doc);
session.endSession();

这个例子里,读之所以一定能看到写,不是因为读了主节点,而是因为会话把前后两次操作串成了一条因果链:写返回的 operationTime 成为读的 afterClusterTime 下界。它帮助理解的是"一致性可以按会话局部保证",不能替代的部分是跨会话、跨用户的全局一致------两个不同用户之间没有因果链,A 刚写的数据 B 不一定立刻看到。

因果一致性有明显的边界。它要求写使用 majority 或至少能被多数派确认,否则位点信息不足以在从节点上等待;它要求会话内串行,会话之间的并发不受保护;它也会让读略微变慢,因为从节点可能需要等待复制追平。适合的场景是"同一用户同一次操作链路内的写后读",例如下单后跳转详情、改密后立即校验。

7. 多数派读与主从延迟:真正决定体验的两个变量

多数派读(readConcern: majority)保证读到的数据已被多数派确认,不会读到随后被回滚的写。但它没有承诺"最新":如果主节点已写入、多数派也已确认,而从节点还没应用到那个位点,你从从节点做 majority 读仍可能看不到。真正的"最新"需要 readPreference: primary 或 linearizable。

主从延迟(replication lag)是另一个现实变量。复制是异步的,从节点落后几毫秒到几十秒都可能。延迟的来源包括网络抖动、从节点磁盘慢、大批量写导致 oplog 应用排队、从节点上有重查询抢占资源。延迟高时,secondaryPreferred 读到旧数据的概率显著上升,maxStalenessSeconds 会把这类节点排除,但代价是可用候选减少,极端情况下读被迫回主节点。

text 复制代码
写请求              主节点 P            从节点 S2
  |                   |                   |
  |-- insert O3001 -->|                   |
  |   返回 w:majority |--- oplog 复制 --->| (延迟 8s 应用)
  |                   |                   |
  |-- find O3001 ------------------------->|
  |   readConcern:local, readPref:secondary| 读到 null
  |<---------------------------------------|
  |                    |                   |
  |-- find O3001(会话 afterClusterTime) -->|
  |   等待 S2 应用到位点后再返回              | 读到 O3001
  |<---------------------------------------|

这张时序图想说明:同样的"从节点读",加不加因果位点等待,结果完全不同。它不能替代的是复制协议细节,例如选举、回滚与 oplog 窗口,这些在副本集故障切换时才真正主导行为。工程上更有用的判断是:先量出当前复制延迟分布,再决定哪些读可以容忍旧值。

8. 在 Spring Boot 里把三者组合起来

Spring Data MongoDB 通过 MongoTemplate 的 Query、WriteConcern 以及 ClientSession 暴露这些旋钮。下面三个完整示例按难度递进:先验证 writeConcern 与 readConcern 的基本效果,再用 causal session 修复写后读,最后给出一个可排障的延迟观测片段。

8.1 示例一:验证写关注与读关注的基本行为

目标:在本地三节点副本集(可用 Docker 或 Atlas 免费集群)中插入一条订单,分别用 local 与 majority 读,观察返回差异。前置环境:MongoDB 5.0+ 副本集,Spring Boot 3.x,依赖 spring-boot-starter-data-mongodb;数据库 shop,集合 orders。

java 复制代码
package com.example.mongo;

import com.mongodb.ReadConcern;
import com.mongodb.WriteConcern;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;

import java.math.BigDecimal;

@SpringBootApplication
public class ConcernDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConcernDemoApplication.class, args);
    }

    @Bean
    CommandLineRunner run(MongoTemplate template) {
        return args -> {
            template.getCollection("orders").drop();

            Order order = new Order("O4001", "U9", new BigDecimal("199.00"), "CREATED");
            // 写关注 majority:多数派确认后才返回
            template.withWriteConcern(WriteConcern.MAJORITY).insert(order);

            Query q = Query.query(Criteria.where("orderId").is("O4001"));
            // 读关注 local:本节点可见即返回
            Order localRead = template.withReadConcern(ReadConcern.LOCAL)
                    .findOne(q, Order.class);
            Order majorityRead = template.withReadConcern(ReadConcern.MAJORITY)
                    .findOne(q, Order.class);

            System.out.println("local read   = " + localRead);
            System.out.println("majority read= " + majorityRead);
        };
    }
}

配套实体类需要显式映射集合名,否则默认集合是类名首字母小写。

java 复制代码
package com.example.mongo;

import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

import java.math.BigDecimal;

@Document(collection = "orders")
public class Order {

    @Field("orderId")
    private String orderId;
    private String userId;
    private BigDecimal amount;
    private String status;

    public Order() {
    }

    public Order(String orderId, String userId, BigDecimal amount, String status) {
        this.orderId = orderId;
        this.userId = userId;
        this.amount = amount;
        this.status = status;
    }

    public String getOrderId() { return orderId; }
    public String getUserId() { return userId; }
    public BigDecimal getAmount() { return amount; }
    public String getStatus() { return status; }

    @Override
    public String toString() {
        return "Order{" + orderId + "," + userId + "," + amount + "," + status + "}";
    }
}

关键步骤与预期结果:写使用 majority 后,两次读通常都能返回同一条记录,因为多数派已确认。把写改成 WriteConcern.W1 并在读之前立刻把请求指向从节点(readPreference: secondary),就可能出现 local 读到 null、majority 稍后读到的现象。适用场景是验证与压测;容易改错的地方是:withWriteConcern 返回新的 MongoTemplate,必须用返回值继续操作,否则原对象仍是默认关注级别。

8.2 示例二:用 causal session 修复"下单后立刻查不到"

目标:模拟一次写主节点、读从节点的业务链路,用会话保证同一链路内写后立即可读。前置环境同上,另需确认连接串开启了副本集发现,例如 mongodb://localhost:27017,localhost:27018,localhost:27019/shop?replicaSet=rs0。

java 复制代码
package com.example.mongo;

import com.mongodb.ReadConcern;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import org.bson.Document;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CausalSessionConfig {

    @Bean
    CommandLineRunner causalDemo(MongoClient client) {
        return args -> {
            MongoDatabase db = client.getDatabase("shop");
            MongoCollection<Document> orders = db.getCollection("orders");

            // 会话级别开启因果一致
            try (ClientSession session = client.startSession(
                    com.mongodb.ClientSessionOptions.builder()
                            .causallyConsistent(true).build())) {

                orders.insertOne(session,
                        new Document("orderId", "O5001")
                                .append("userId", "U9")
                                .append("status", "PAID")
                                .withWriteConcern(WriteConcern.MAJORITY));

                // 同一会话内的读,驱动自动带 afterClusterTime
                Document found = orders.withReadPreference(ReadPreference.secondaryPreferred())
                        .withReadConcern(ReadConcern.MAJORITY)
                        .find(session, Filters.eq("orderId", "O5001"))
                        .first();

                System.out.println("causal read = " + found);
            } catch (Exception e) {
                // 会话必须在 finally/ try-with-resources 中释放,否则连接池会被占用
                throw new IllegalStateException("causal demo failed", e);
            }
        };
    }
}

关键步骤与可观察结果:写使用 majority 后,会话记录了 operationTime;随后的从节点读携带 afterClusterTime,如果从节点尚未追平,服务端会等待而不是返回 null。适用场景是"下单后跳详情""改状态后立即展示"等同一用户链路。容易改错的地方有三处:事务或会话必须用 try-with-resources 释放;因果一致要求写关注至少是 majority,否则位点不可靠;读取仍要显式指定 readConcern majority 与允许的 readPreference,否则可能因为节点选择而绕过等待。

8.3 示例三:观测主从延迟并决定读路由

目标:在应用启动时打印各节点复制延迟,为"哪些读能走从节点"提供依据。前置环境:副本集连接串,且账号有执行 serverStatus、replSetGetStatus 的权限。

java 复制代码
package com.example.mongo;

import com.mongodb.MongoException;
import com.mongodb.client.MongoClient;
import org.bson.Document;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class LagProbeConfig {

    @Bean
    CommandLineRunner lagProbe(MongoClient client) {
        return args -> {
            try {
                Document status = client.getDatabase("admin")
                        .runCommand(new Document("replSetGetStatus", 1));

                // members 中每个节点带 optimeDate 与 stateStr
                for (Document member : status.getList("members", Document.class)) {
                    String name = member.getString("name");
                    String state = member.getString("stateStr");
                    Object optime = member.get("optimeDate");
                    System.out.printf("member=%s state=%s optime=%s%n", name, state, optime);
                }
            } catch (MongoException e) {
                // 单节点或权限不足时降级,不影响主流程
                System.err.println("replSetGetStatus failed: " + e.getMessage());
            }
        };
    }
}

关键步骤与可观察结果:optimeDate 反映各节点最近应用的 oplog 时间,主节点与从节点之差即为大致延迟。若发现某从节点长期落后几十秒,就不应该让它承接对新鲜度敏感的业务读。适用场景是上线前的容量与拓扑检查、故障时的快速定位;边界是该方法为采样观测,不能替代持续监控,且权限不足时会抛错,需要做降级处理。

9. 把三组旋钮组合成决策

到这里可以把分散的知识收回一张决策表。选择顺序建议是:先问写能不能丢,再问读能不能旧,最后问读能不能从从节点走。

业务诉求 writeConcern readConcern readPreference 备注
埋点、日志 w:1 local secondaryPreferred 可容忍丢失与旧读
商品列表、报表 w:1 local secondaryPreferred + maxStaleness 用陈旧度阈值兜底
下单、支付 w:majority majority 或 causal session primary 或会话内从节点读 优先正确性
同一用户写后读 w:majority majority + afterClusterTime secondaryPreferred 可用 会话串因果
强一致单文档读 w:majority linearizable primary 代价最高,慎用

决策之外还有容量视角:读走从节点能显著降低主节点压力,但前提是复制延迟可控;writeConcern 提到 majority 会让写延迟上升,需要评估连接池与超时配置,避免把超时误判为失败并重复写。跨机房部署时,nearest 看似降低延迟,但可能把读导向异地副本,读到更旧的数据,需要与 maxStalenessSeconds 一起使用。

10. 常见误区

误区一:以为 w:majority 就等于强一致读。它只保证写被多数派确认,不保证后续的从节点读能看到最新值;要写后立刻读,需要 readConcern majority 配合因果会话或读主节点。

误区二:以为 readConcern: majority 就是"读最新"。它只排除未多数派确认的数据,仍可能落后于主节点当前写入;真正的"最新"是 linearizable 或读主节点。

误区三:把 w:0 当成高性能万能药。它在写路径上不等确认,主节点故障时这些写可能直接丢失,且驱动可能在网络错误时无法区分成功与失败。

误区四:跨线程复用同一个会话做并发操作。会话内因果顺序是按操作串行维护的,并发使用同一会话会破坏因果链,驱动也可能报错。每个请求链路使用独立会话,并在结束时释放。

误区五:把 secondaryPreferred 当作默认优化。它在从节点资源紧张或延迟升高时会把旧数据暴露给用户,必须先量化延迟再决定。

11. 生产实践建议

第一,把一致性要求写进接口契约。订单、资金类接口固定 w:majority 与读主节点或因果会话;监控、报表类接口明确允许 secondaryPreferred 与陈旧度上限。不要用一套默认值覆盖所有接口。

第二,给写关注设置合理的 wtimeout,并在超时后按"结果未知"处理:用业务唯一键做幂等判断,或先查一次再决定是否重试。把超时当成失败并盲目重试,是重复写入的主要来源。

第三,监控复制延迟并设置告警阈值。常见指标是各从节点 optimeDate 与主节点的差值、oplog 窗口大小、从节点磁盘与应用线程状况。延迟持续升高时,先降级读路由(回主节点)再排查。

第四,连接池与超时要与一致性级别匹配。majority 写会占用连接更久,池过小会导致排队放大延迟;读从节点时要注意驱动拓扑刷新周期,避免节点角色变化后仍路由到旧目标。

第五,把 readPreference 标签与机房规划绑定。跨机房部署用 tag 精确控制读目标,配合 maxStalenessSeconds 防止读到异地旧数据,并在机房故障演练中验证切换到主节点的行为。

12. 排障清单

遇到"刚写读不到""切主后数据消失""从节点读延迟高"时,按下面顺序查。

  • 确认写入返回的关注级别与实际生效值:驱动上的 WriteConcern 是否被覆盖,是否经过 withWriteConcern 返回值继续操作。
  • 确认读请求的 readPreference 与命中的节点:开启驱动日志或使用 $currentOp、serverStatus 观察连接。
  • 量出复制延迟:replSetGetStatus 的 optimeDate 差,以及从节点错误日志。
  • 检查是否混用了会话:同一会话是否被并发使用,是否在异常路径未释放。
  • 检查是否有回滚:主节点日志中的 rollback 记录会解释"读到的数据消失"。
  • 检查 readConcern 是否与 readPreference 冲突:linearizable 只能走主节点,误配会被驱动拒绝或报错。
  • 检查网络与拓扑:应用是否连到了旧的主节点地址,拓扑刷新是否及时。

13. 面试/复盘问题

  1. 为什么 w:majority 写成功后,从节点仍可能读不到?请从复制路径与 readConcern 两个角度解释。
  2. 因果一致性依赖哪个服务端返回值来串起写与读?它为什么要求写关注至少是 majority?
  3. readConcern: majority 与 readConcern: linearizable 的区别是什么?各自能否走从节点?
  4. 读写分离下,如何既保留从节点读的吞吐又避免用户看到旧数据?请给出至少两种方案并说明代价。
  5. maxStalenessSeconds 能解决什么问题,不能解决什么问题?
  6. 主从延迟突然升高时,你会先调整应用侧还是数据库侧?为什么?

14. 总结

回到开头那个"下单后查不到"的场景,根因不是数据丢了,而是写路径与读路径没有对齐:写只承诺到主节点或多数派,读却去了一个还没追平的从节点。把 writeConcern、readConcern、readPreference 三者拆开看,再理解 causal session 如何用 afterClusterTime 把一次业务链路串起来,就能对每个接口给出明确的一致性承诺,而不是碰运气。

工程上的落地顺序可以归纳为:先用 w:majority 保护不能丢的写;再按"能否读到回滚数据"决定 readConcern;最后按"能否容忍旧值"决定 readPreference 与陈旧度阈值;需要同一链路写后读时,用因果会话补上顺序保证。每组选择都有可量化的代价------写延迟、读延迟、主节点压力、可用候选减少------把这些代价显式写进设计文档,远比记住一组"最佳配置"更可靠。

15. 参考资料

相关推荐
天空属于哈夫克31 小时前
企业微信消息回调处理:如何避免慢查询拖垮整个服务?
数据库·php·企业微信
烟雨归来2 小时前
Oracle备份NFS IO挂死引发PQ并行进程耗尽process
数据库·oracle
acd120092 小时前
PHP 8.1 下 Swoole 协程 PDO 偶尔出现 MySQL 连接丢失问题排查
数据库·oracle
cc5725026532 小时前
2027校招:大数据管理与应用转财务数字化,会计业务、SQL与BI学习路线
数据库·sql·学习
梦想不只是梦与想2 小时前
SQLAlchemy:异步、连接池 (四)
数据库·fastapi·sqlalchemy
j7~2 小时前
【仿 RabbitMQ 项目】发布订阅消息队列 (篇八):交换机数据管理模块,分层类设计、SQLite 回调原理与代码实现
服务器·网络·数据库·消息队列·交换机·交换机数据管理·sqlite回调原理
PHP实战开发录2 小时前
PHP上传文件为什么会被覆盖
数据库·mysql·php
SelectDB技术团队3 小时前
VARIANT 列接入网关 JSON 日志:建表、导入、子列查询与物化的完整步骤
大数据·数据库·人工智能·elasticsearch·搜索引擎·json·apache doris
涛思数据(TDengine)3 小时前
TDengine IDMP 2.0.2 发布:元素关系可视化、数据建模与联邦查询三大升级
大数据·数据库·人工智能·时序数据库·tdengine·工业互联网·工业ai