第19章:Mongo读写关注与一致性模型——下单后为什么查不到订单

📅 发布时间:2026/7/20 14:53:45
第19章:Mongo读写关注与一致性模型——下单后为什么查不到订单 1. 项目背景业务场景本地生活电商的订单系统切换到了复制集看起来一切正常——直到客服接到大量投诉“我刚下单成功了但打开’我的订单’页面根本看不到这笔订单”我明明付了款订单状态还是’待支付’过了一分钟才变。技术团队排查发现下单接口连接的是 Primary 写入订单但我的订单页面为了分摊读压力读的是 Secondary——而 Secondary 的复制延迟 5-8 秒用户刚写入就查从库自然看不到。还有一个更严重的问题——退款流程中先标记订单为已退款再调用支付宝退款如果支付宝退款成功但 MongoDB 的订单状态更新因为从库落后没被后续流程读到后果是财务部门统计到已退款但支付宝实际未退款的对账异常。痛点writeConcern、readConcern、readPreference 这三个参数是 MongoDB 一致性的三驾马车90% 的开发从来没配过也不知道默认值是什么意思。典型问题“readConcern: local和majority的区别是什么”“我的写入指定了w: majority为什么读还是可能读到旧数据”“读 Primary 和读 Secondary 到底差了什么”“因果一致性怎么配”2. 项目设计小胖抱头大师我刚下的订单在列表页看不到过 5 秒刷新又有了。这特么是在变魔术吗大师不是魔术是你在 Primary 上写下单在 Secondary 上读列表页查询。Secondary 复制需要时间——这个时间差就是你观察到的幽灵期。这引出 MongoDB 三个核心概念writeConcern写关注即你的写入要多可靠。w: 1只等 Primary 确认快但不稳w: majority等多数节点确认稳但稍慢。readConcern读关注即你的读取要看到哪个时间点之后的数据。local是最新的可能还未被确认majority是已被多数节点确认过的已持久化。readPreference读偏好即你的请求发给谁。primary读主节点secondary读从节点。小胖那我的场景——在下单和列表页之间保证一致性应该怎么配大师这叫读己之写Read Your Own Writes。解决方案之一是因果一致性Causal Consistency。你只需要在 Spring Boot 中做两件事// 写入时下单接口ClientSessionsessionclient.startSession();session.startTransaction();// ... 写入订单 ...session.commitTransaction();// 读取时我的订单接口// 通过 session 传递 afterClusterTime确保读到本次写入之后的数据如果写入和读取不在同一个 session 的上下文中比如跨服务最简单的做法是——读你的写入不要从 Secondary 读——把我的订单这个接口的readPreference强制设为primary。技术映射MongoDB 的因果一致性通过afterClusterTime和operationTime实现——写入返回一个时间戳后续读取带上这个时间戳保证读到的数据不早于该时间戳。小白疑惑那readConcern: linearizable呢是不是最严格的MongoDB 支持吗大师支持但不推荐在生产中频繁使用。linearizable要求读操作必须被当前 Primary 处理并且确认 Primary 仍然是真正的 Primary——它消耗额外的一次多数确认通信。大多数业务用majority就够了。对账/金融的精确总账可以用linearizable确保不会读到已经回滚的写入。技术映射readConcern的严格程度排序local(最快) availablemajoritylinearizable(最严格) snapshot事务专用。小白那不同业务场景该用什么配置能用一张表说清楚吗大师看这张业务场景配置表业务场景writeConcernreadPreferencereadConcern理由用户下单majorityprimarylocal写是要紧操作读直接从主读商品浏览w: 1secondarylocal允许短暂不一致高吞吐订单列表我的订单majorityprimarylocal读己之写必须读主运营报表w: 1secondarymajority允许延迟但不要瞬态数据库存扣减majorityprimarysnapshot事务内确切减库存不能丢对账/财务majorityj: trueprimarylinearizable精确一致零容忍小胖等等j: true又是什么大师j代表 Journal——WiredTiger 的预写日志。j: true要求写入被刷新到磁盘的 Journal 文件后才返回成功。这避免掉电丢失已缓冲但未落盘的数据。代价是额外 IO 延迟。技术映射j: true≈ MySQL 的innodb_flush_log_at_trx_commit1。是阻止断电丢数据的最后一层保障。大师总结一致性不是越严格越好——每个场景按自己的要求选配。今天的核心记三个数——读什么readPreference读到什么版本readConcern写多保险writeConcern。三者各自独立组合起来才是一致性策略。3. 项目实战3.1 环境准备沿用第 17 章的 3 节点复制集。3.2 分步实现步骤一演示读己之写问题目标在 Primary 上写入后在 Secondary 上查询复现查不到的问题。// 连接 Primary// mongosh mongodb://localhost:27017/?replicaSetmyRSuse local_lifeconsttestDoc{testId:RW_TEST_Date.now(),value:刚写入的数据,createdAt:newDate()}db.rw_test.insertOne(testDoc,{writeConcern:{w:1}})print(写入完成:,testDoc.testId)// 立即在 Secondary 上查询// mongosh mongodb://localhost:27018/?replicaSetmyRSreadPreferencesecondarydb.getSiblingDB(local_life).rw_test.findOne({testId:testDoc.testId})// 如果刚写入立即查可能会返回 null——Secondary 还没同步到步骤二五种 writeConcern 实测对比目标通过计时对比不同 writeConcern 的性能影响。use local_lifefunctiontestWriteConcern(w,jfalse,label){conststartDate.now()try{db.wc_test.insertOne({label:label,ts:newDate(),w:String(w)},{writeConcern:{w:w,j:j,wtimeout:5000}})constelapsedDate.now()-startprint(${label}(w:${w}, j:${j}):${elapsed}ms)returnelapsed}catch(e){print(${label}: FAILED -${e.message})return-1}}testWriteConcern(0,false,不确认)testWriteConcern(1,false,主节点确认)testWriteConcern(majority,false,多数确认)testWriteConcern(1,true,主节点Journal)testWriteConcern(majority,true,多数Journal)// 期望耗时越来越长步骤三四种 readConcern 对比目标理解不同 readConcern 返回数据的时间点差异。// 在不同 readConcern 下读同一个文档functiontestReadConcern(level,label){conststartDate.now()constdocdb.rw_test.findOne({testId:{$regex:/^RW_TEST/}},{},{readConcern:{level:level}})constelapsedDate.now()-startprint(${label}:${elapsed}ms -${doc?有数据:无数据(可能还未同步)})returndoc}// 注意readConcern 需要在 mongosh 中通过命令或者通过 session 指定// 使用 runCommand 方式测试constresultdb.runCommand({find:rw_test,filter:{testId:{$regex:/^RW_TEST/}},readConcern:{level:local}})print(local 结果数:,result.cursor.firstBatch.length)constresult2db.runCommand({find:rw_test,filter:{testId:{$regex:/^RW_TEST/}},readConcern:{level:majority}})print(majority 结果数:,result2.cursor.firstBatch.length)// 在从库上majority 可能会少返回刚写入但未多数确认的文档步骤四因果一致性实战目标通过 session 传递operationTime实现因果一致性。// 场景同一个 session 内写入后立刻读取保证读到刚写的constsessiondb.getMongo().startSession()constcollsession.getDatabase(local_life).rw_test// 写入constwriteResultcoll.insertOne({testId:CAUSAL_TEST,value:因果一致性测试,createdAt:newDate()},{writeConcern:{w:majority}})print(写入 operationTime:,writeResult.operationTime)// 使用同一个 session 读取自动携带 causalConsistencyconstreadResultcoll.findOne({testId:CAUSAL_TEST},{readConcern:{level:majority}})print(读取结果:,readResult?读到刚写入的数据:未读到不应发生)session.endSession()// 跨 session 的因果一致性// 如果写入和读取不在同一 session手动传递 operationTime// const readResult2 coll.findOne(// { testId: CAUSAL_TEST },// {// readConcern: {// level: majority,// afterClusterTime: writeResult.operationTime// }// }// )步骤五readPreference 组合测试目标验证不同 readPreference 的实际行为。// 在 Primary 上写入db.rp_test.insertOne({value:RP_TEST,createdAt:newDate()},{writeConcern:{w:majority}})// 测试 1primary 读默认constpdb.runCommand({find:rp_test,filter:{value:RP_TEST},$readPreference:{mode:primary}})print(primary 读主库:,p.cursor.firstBatch.length0?PASS:FAIL)// 测试 2secondary 读// 在 mongosh 连接时指定: mongosh .../?readPreferencesecondary// 或在查询中:constsdb.runCommand({find:rp_test,filter:{value:RP_TEST},$readPreference:{mode:secondary}})print(secondary 读从库:,s.cursor.firstBatch.length0?PASS:FAIL)// 测试 3nearest 读最低延迟节点constndb.runCommand({find:rp_test,filter:{value:RP_TEST},$readPreference:{mode:nearest}})print(nearest 读:,n.cursor.firstBatch.length0?PASS:FAIL)步骤六一致性配置对照表目标综合理解 writeConcern readConcern readPreference 的配合。// 场景 A电商下单强一致 // write: { w: majority, j: true }// read: { readConcern: { level: local }, readPreference: primary }// 结果写入需要多数确认读从主库读最新数据// 场景 B商品浏览高性能 // write: { w: 1 }// read: { readConcern: { level: local }, readPreference: secondary }// 结果写入只等 Primary读从从库分担读压力// 场景 C报表允许延迟不要瞬态 // write: { w: 1 }// read: { readConcern: { level: majority }, readPreference: secondary }// 结果读到的是多数确认过的稳定数据但可能有延迟// 场景 D金融对账最高一致 // write: { w: majority, j: true }// read: { readConcern: { level: linearizable }, readPreference: primary }// 结果写入需 journal 持久化 多数确认读需要验证 Primary 身份// 验证脚本 constscenarios[{name:电商下单,w:majority,rc:local,rp:primary},{name:商品浏览,w:1,rc:local,rp:secondary},{name:报表,w:1,rc:majority,rp:secondary},{name:金融对账,w:majority,rc:linearizable,rp:primary}]console.table(scenarios)3.3 完整代码清单文件用途mongodb-lab/replicaset/read-write-concern.jswriteConcern/readConcern 对比实验mongodb-lab/replicaset/causal-consistency.js因果一致性演示mongodb-lab/replicaset/read-preference.jsreadPreference 行为测试mongodb-lab/replicaset/consistency-matrix.js业务场景一致性配置矩阵3.4 测试验证// 1. 验证 writeConcern: majority 的可靠性db.test_wc.insertOne({test:wc_majority},{writeConcern:{w:majority}})// 在 2 个 Secondary 上验证数据存在// → 全部通过// 2. 验证因果一致性constsessdb.getMongo().startSession()sess.getDatabase(local_life).test_causal.insertOne({_id:CT1},{writeConcern:{w:majority}})constressess.getDatabase(local_life).test_causal.findOne({_id:CT1})print(因果一致性:,res!null?PASS:FAIL)sess.endSession()// 3. 验证 readConcern: linearizabletry{db.runCommand({find:test_wc,readConcern:{level:linearizable}})print(linearizable: PASS)}catch(e){print(linearizable: 仅 Primary 支持 - e.message)}print(\n 一致性验证完成 )4. 项目总结4.1 一致性参数速查参数可选值默认值性能代价安全级别writeConcern.w0/1/n/“majority”1w↑ 延迟↑w↑ 安全↑writeConcern.jtrue/falsefalse64位平台jtrue 延迟↑磁盘 IOjtrue 防断电丢数据readConcern.levellocal/available/majority/linearizable/snapshotlocallinearizable 性能↓majority 防脏读readPreferenceprimary/primaryPreferred/secondary/secondaryPreferred/nearestprimary无显著差异路由开销primary 一致性最强4.2 适用场景各配置组合的典型场景已在步骤六中给出。4.3 注意事项注意事项说明readConcern: majority在从库可能阻塞从库需要检查 Primary 的 commit point如果主从延迟大读会等不要混用w:1和linearizable写入只等 Primary读却检查 Primary 是否仍为 Primary——逻辑矛盾因果一致性的 session 不能跨服务如果下单和订单列表是不同的微服务需要显式传递operationTimenearest可能读到自己的写入失败nearest 按网络延迟路由可能将刚写入的请求的后续读请求发送至延迟大的从库仲裁节点无数据readPreference: secondary时不会路由到 Arbiter4.4 常见踩坑经验故障案例一读从库看到幽灵订单某系统用writeConcern: 1写订单但用readConcern: local从从库读——巧合读到一条刚写入但 Primary 立刻宕机后被回滚的订单。根因从库在回滚前已经拉取了这条 Oplog 并应用了但没来得及知道这条记录已被回滚。解决将读升级到readConcern: majority多数确认的数据不会回滚。故障案例二w: majority在 2 节点复制集中卡住已在上一章中讲过此案例本章角度不同——开发在应用代码中写死了w: majority运维因资源紧张只给了 2 个数据节点——系统间歇性报waiting for replication timed out。解决要么加数据节点要么业务分级——部分低优先级写入降级为w:1。故障案例三nearestsecondary导致请求漂移某微服务配置readPreference: nearest在 K8s 的多可用区部署中客户端到不同分区的从库延迟有波动导致同一用户的请求一会儿读北京从库、一会儿读上海从库——订单列表数据来回变动。解决用户相关接口强制使用readPreference: primary或primaryPreferred。4.5 思考题如果使用writeConcern: majorityreadConcern: majority的组合能保证写后立刻读总能读到刚写的数据吗为什么在 Spring Boot 中如何为某个特定查询覆写全局的 readPreference如何验证这个覆写真的生效了答案将在第 20 章末尾揭晓上一章思考题答案三节点全部宕机后应该先启动最后一个 Secondary即拥有最新 Oplog 的节点让它作为恢复的基准。如果启动了最旧的节点且它被选举为 Primary后续节点需要通过 Rollback 把多余的操作回滚掉 可能引发数据丢失。正确顺序① 找到所有节点中最新的 Oplog看rs.status()保存在每个节点 local 数据库的历史信息② 先启动拥有最新 Oplog 的节点让它成为 Primary③ 再启动其他节点让其追赶。Hidden 节点实时拉取 Oplog只是不对外暴露读接口hidden: true阻止客户端驱动的读路由。Delayed 节点故意延迟Oplog 的应用secondaryDelaySecs秒后再应用Oplog仍然被拉取但被缓存在本地延迟执行。区别在于Hidden 节点的数据是准实时的和普通 Secondary 一样Delayed 节点的数据是故意滞后的。延伸阅读与资源python入门Rquests从菜鸟脚本到企业级SDK的网络实战圣经Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析