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. 面试/复盘问题
- 为什么
w:majority写成功后,从节点仍可能读不到?请从复制路径与 readConcern 两个角度解释。 - 因果一致性依赖哪个服务端返回值来串起写与读?它为什么要求写关注至少是 majority?
readConcern: majority与readConcern: linearizable的区别是什么?各自能否走从节点?- 读写分离下,如何既保留从节点读的吞吐又避免用户看到旧数据?请给出至少两种方案并说明代价。
maxStalenessSeconds能解决什么问题,不能解决什么问题?- 主从延迟突然升高时,你会先调整应用侧还是数据库侧?为什么?
14. 总结
回到开头那个"下单后查不到"的场景,根因不是数据丢了,而是写路径与读路径没有对齐:写只承诺到主节点或多数派,读却去了一个还没追平的从节点。把 writeConcern、readConcern、readPreference 三者拆开看,再理解 causal session 如何用 afterClusterTime 把一次业务链路串起来,就能对每个接口给出明确的一致性承诺,而不是碰运气。
工程上的落地顺序可以归纳为:先用 w:majority 保护不能丢的写;再按"能否读到回滚数据"决定 readConcern;最后按"能否容忍旧值"决定 readPreference 与陈旧度阈值;需要同一链路写后读时,用因果会话补上顺序保证。每组选择都有可量化的代价------写延迟、读延迟、主节点压力、可用候选减少------把这些代价显式写进设计文档,远比记住一组"最佳配置"更可靠。
15. 参考资料
- MongoDB 官方文档:Read Concern(https://www.mongodb.com/docs/manual/reference/read-concern/)
- MongoDB 官方文档:Write Concern(https://www.mongodb.com/docs/manual/reference/write-concern/)
- MongoDB 官方文档:Read Preference(https://www.mongodb.com/docs/manual/core/read-preference/)
- MongoDB 官方文档:Causal Consistency and Read and Write Concerns(https://www.mongodb.com/docs/manual/core/causal-consistency-read-write-concerns/)
- MongoDB 官方文档:Replica Set Member States 与 Replication Lag(https://www.mongodb.com/docs/manual/replication/)
- Spring Data MongoDB 官方文档:MongoTemplate 与 ClientSession(https://docs.spring.io/spring-data/mongodb/reference/)