Read and write concerns
replica set เก็บข้อมูลของคุณหลายชุด: primary หนึ่งตัวที่รับการเขียนทั้งหมด และ secondary หนึ่งตัวหรือมากกว่าที่คัดลอกการเขียนเหล่านั้นไป ความซ้ำซ้อนนี้คือสิ่งที่ทำให้ปุ่มปรับความคงทนและการมองเห็นมีความหมาย เมื่อเขียน คุณกำหนดได้ว่าต้องมีกี่สำเนายืนยันการเขียนก่อนจะถือว่าเสร็จ เมื่ออ่าน คุณกำหนดได้ว่าข้อมูลต้อง commit แล้วและสดแค่ไหน สองการตั้งค่านี้คือ write concern และ read concern
output มาจากสองที่ ทั้งคู่บนเครื่อง Mac นี้ด้วย mongo 8.3.11, mongosh 2.11.1 และ Node driver 7.7.0: fence ที่มี path comment รันบน replica set โหนดเดียวของคอร์ส; fence ที่ระบุว่า 3-node รันบน replica set สามคอนเทนเนอร์ชั่วคราวบน Docker network (rs-1, rs-2, rs-3 สตาร์ทด้วย --replSet rsb3) ซึ่งลบทิ้งหลังใช้งาน แท็บ Python/Go/Rust สะท้อนการเรียกที่ตรวจแล้วใน shell/Node และไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส
Write concern: คงทนแค่ไหน ยืนยันแค่ไหน
หัวข้อที่มีชื่อว่า “Write concern: คงทนแค่ไหน ยืนยันแค่ไหน”write concern มีสามส่วน w บอกว่ามีสมาชิกกี่ตัวต้องยืนยัน: w: 1 คือเฉพาะ primary, w: "majority" คือเสียงข้างมากของสมาชิกที่มีสิทธิ์โหวตมีการเขียนนั้นแล้ว j เมื่อเป็น true ต้องให้การเขียนอยู่ใน journal บนดิสก์ของสมาชิกที่นับ wtimeout (option ของ driver คือ wtimeoutMS) จำกัดเวลาที่ server รอการยืนยันเหล่านั้น
db.members.insertMany([ { name: "Ada", fines: 0 }, { name: "Linus", fines: 2 },]);assert.strictEqual(db.members.countDocuments(), 2);insert สมาชิกและกำหนดให้ต้องได้ majority ที่ journal แล้ว ภายในห้าวินาที:
const res = db.members.insertOne( { name: "Grace", fines: 0 }, { writeConcern: { w: "majority", j: true, wtimeout: 5000 } });console.log(res);assert.ok(res.acknowledged);const res = await db.collection("members").insertOne( { name: "Grace", fines: 0 }, { writeConcern: { w: "majority", j: true, wtimeoutMS: 5000 } });console.log(res);if (!res.acknowledged) throw new Error("expected an acknowledged write");from pymongo import WriteConcern
db.members.with_options( write_concern=WriteConcern(w="majority", j=True, wtimeout=5000)).insert_one({"name": "Grace", "fines": 0})wc := writeconcern.Majority()coll := client.Database("library").Collection( "members", options.Collection().SetWriteConcern(wc),)_, err := coll.InsertOne(ctx, bson.M{"name": "Grace", "fines": 0})if err != nil { return err}let wc = WriteConcern::builder() .w(Acknowledgment::Majority) .journal(true) .build();let coll = client .database("library") .collection::<Document>("members") .clone_with_options(CollectionOptions::builder().write_concern(wc).build());coll.insert_one(doc! { "name": "Grace", "fines": 0 }).await?;{ acknowledged: true, insertedId: ObjectId('6abccc3967d5308401ddf857')}{ acknowledged: true, insertedId: new ObjectId('6abccc39a50eaebf0946af88')}คุณมี w: "majority" อยู่แล้ว
หัวข้อที่มีชื่อว่า “คุณมี w: "majority" อยู่แล้ว”ตั้งแต่ MongoDB 5.0 write concern เริ่มต้นแบบแฝงคือ w: "majority" server รายงานค่านี้ และรายงานว่า majority write รอ journal บนสมาชิกที่นับอยู่แล้ว:
const d = db.adminCommand({ getDefaultRWConcern: 1 });console.log(d.defaultWriteConcern, d.defaultReadConcern);const conf = rs.conf();console.log({ writeConcernMajorityJournalDefault: conf.writeConcernMajorityJournalDefault });assert.strictEqual(d.defaultWriteConcern.w, "majority");assert.strictEqual(conf.writeConcernMajorityJournalDefault, true);{ w: 'majority', wtimeout: 0 } { level: 'local' }{ writeConcernMajorityJournalDefault: true }manual ก็ยืนยันเช่นนั้น (Write Concern, ตรวจเมื่อ 2026-09-30): ค่าเริ่มต้นแบบแฝงคือ w: majority ซึ่งรอ journal บนดิสก์ตาม writeConcernMajorityJournalDefault — มีกรณียกเว้นหนึ่งอย่าง: ถ้าสมาชิกที่เก็บข้อมูลและมีสิทธิ์โหวตไม่มากกว่า voting majority (เซตที่มี arbiter) ค่าเริ่มต้นจะถอยไปเป็น { w: 1 } ดังนั้น insertOne เปล่า ๆ ก็เป็น majority write อยู่แล้ว และ j: true เพิ่มบน w: "majority" ไม่ได้เพิ่มอะไรตราบที่ writeConcernMajorityJournalDefault เป็น true (ค่าเริ่มต้น) มันสำคัญกับ w: 1 ซึ่ง j เป็นตัวตัดสินว่า primary จะรอ flush journal ของตัวเองหรือไม่
เมื่อ concern ทำให้ครบไม่ได้
หัวข้อที่มีชื่อว่า “เมื่อ concern ทำให้ครบไม่ได้”ขอสมาชิกที่เก็บข้อมูลสองตัวบนเซตโหนดเดียว server ปฏิเสธทันที — แต่สังเกตว่าการเขียนยังถูกนำไปใช้แล้ว:
const members = db.collection("members");let err = null;try { await members.insertOne({ name: "X" }, { writeConcern: { w: 2, wtimeoutMS: 1000 } }); }catch (e) { err = e; }console.log(err.name, err.code, err.codeName, "-", err.message);console.log("document present anyway:", await members.countDocuments({ name: "X" }));if (err.code !== 100) throw new Error("expected UnsatisfiableWriteConcern (100)");MongoWriteConcernError 100 UnsatisfiableWriteConcern - Not enough data-bearing nodesdocument present anyway: 1error ของ write concern ไม่ใช่ rollback (manual: wtimeout “does not undo successful data modifications”): มันบอกว่า “ฉันยืนยันสำเนาได้ไม่พอ” ไม่ใช่ “ฉันไม่ได้เขียน” นี่คือเหตุผลที่ wtimeout เป็นผลลัพธ์ที่ไม่แน่นอน ไม่ใช่ความล้มเหลวที่สะอาด การรัน 3-node แสดงแบบ timeout — secondary หนึ่งตัวถูกแช่แข็งด้วย fsyncLock, เขียน w: 3 พร้อม wtimeout: 2000 (การเขียนอื่นในรอบเดียวกัน: w: 1 ได้ ack, w: "majority" ได้ ack ใน 3 ms เพราะ secondary อีกตัวยังนับอยู่):
// mongosh/concern-timeout.js @skip-verify needs the 3-node replica set (real output below)const lib = db.getSiblingDB("lib");lib.members.insertOne({ name: "Grace" }, { writeConcern: { w: 1 } });lib.members.insertOne({ name: "Linus" }, { writeConcern: { w: "majority", wtimeout: 5000 } });try { lib.members.insertOne({ name: "Edsger" }, { writeConcern: { w: 3, wtimeout: 2000 } }); }catch (e) { console.log(e.name, e.code, e.message); console.log(e.errInfo); }w:1 ack truew:majority with one secondary frozen -> true 3 msMongoWriteConcernError | 64 | waiting for replication timed out{ wtimeout: true, writeConcern: { w: 3, wtimeout: 2000, provenance: 'clientSupplied' } }primary has 4 docs (the w:3 write is applied even though it errored)frozen secondary sees 1 docshealthy secondary sees 4Read concern: commit แล้วแค่ไหน
หัวข้อที่มีชื่อว่า “Read concern: commit แล้วแค่ไหน”read concern ควบคุมว่าการอ่านคืนข้อมูลเวอร์ชันไหนได้ local (ค่าเริ่มต้นสำหรับการอ่านนอก transaction) คืนสิ่งที่สมาชิกที่ถูก query มีตอนนี้ แม้เป็นข้อมูลที่อาจถูก roll back ภายหลัง available บน replica set เหมือนกับ local majority คืนเฉพาะข้อมูลที่เสียงข้างมากยืนยันแล้ว snapshot อิงกับ majority และออกแบบไว้สำหรับ transaction และการอ่านหลายคำสั่ง linearizable สะท้อนทุกการเขียนที่เสียงข้างมากยืนยันและเสร็จก่อนการอ่านจะเริ่ม และใช้ได้กับ primary เท่านั้น ทั้งห้าระดับรันได้บนเซตโหนดเดียว:
const counts = {};for (const level of ["local", "available", "majority", "snapshot"]) { counts[level] = db.members.find({ fines: 0 }).readConcern(level).toArray().length;}counts.linearizable = db.members.find({ fines: 0 }).readConcern("linearizable").maxTimeMS(3000).toArray().length;console.log(counts);assert.strictEqual(counts.majority, 2);const members = db.collection("members");await members.deleteMany({}); // fresh startawait members.insertMany([{ name: "Ada", fines: 0 }, { name: "Linus", fines: 2 }]);const counts = {};for (const level of ["local", "available", "majority", "snapshot"]) { counts[level] = (await members.find({ fines: 0 }, { readConcern: { level }, readPreference: "primary" }).toArray()).length;}counts.linearizable = (await members.find({ fines: 0 }, { readConcern: { level: "linearizable" }, maxTimeMS: 3000 }).toArray()).length;console.log(counts);if (counts.majority !== 1) throw new Error("expected one member with fines 0");from pymongo import ReadPreferencefrom pymongo.read_concern import ReadConcern
docs = list( db.members.with_options( read_concern=ReadConcern("majority"), read_preference=ReadPreference.PRIMARY, ).find({"fines": 0}))coll := client.Database("library").Collection( "members", options.Collection(). SetReadConcern(readconcern.Majority()). SetReadPreference(readpref.Primary()),)cur, err := coll.Find(ctx, bson.M{"fines": 0})if err != nil { return err}defer cur.Close(ctx)let coll = client .database("library") .collection::<Document>("members") .clone_with_options( CollectionOptions::builder() .read_concern(ReadConcern::majority()) .selection_criteria(SelectionCriteria::ReadPreference(ReadPreference::Primary)) .build(), );let mut cursor = coll.find(doc! { "fines": 0 }).await?;{ local: 2, available: 2, majority: 2, snapshot: 2, linearizable: 2 }{ local: 1, available: 1, majority: 1, snapshot: 1, linearizable: 1 }แต่ละระดับต่างกันที่ สิ่งที่มันรอ และความต่างนั้นมองเห็นได้แม้บนโหนดเดียว ด้านล่างคือ 200 ครั้งของ: เขียน แล้วนับทันทีด้วย local และ majority — ครั้งหนึ่งกับการเขียนแบบ w: 1 อีกครั้งกับ w: "majority":
const rc = db.collection("rc");await rc.deleteMany({});async function behindCount(w) { let behind = 0; for (let i = 0; i < 200; i++) { await rc.insertOne({ i, w }, { writeConcern: { w } }); const local = await rc.countDocuments({}, { readConcern: { level: "local" } }); const maj = await rc.countDocuments({}, { readConcern: { level: "majority" } }); if (maj < local) behind++; } return behind;}const withW1 = await behindCount(1);await rc.deleteMany({});const withMajority = await behindCount("majority");console.log({ "w:1 then majority-read behind": withW1, "w:majority then majority-read behind": withMajority });if (withMajority !== 0 || withW1 === 0) throw new Error("expected w:1 to lag and w:majority not to");{ 'w:1 then majority-read behind': 200, 'w:majority then majority-read behind': 0}การเขียนแบบ w: 1 ถูก acknowledge ก่อนที่จะถูก commit แบบ majority การอ่านแบบ majority ทันทีหลังจากนั้นจึงไม่เห็นการเขียนของคุณเอง ส่วนการเขียนแบบ w: "majority" ตามด้วยการอ่านแบบ majority เห็นเสมอ (บนโหนดเดียวช่องว่างนี้เป็นระดับมิลลิวินาที ข้ามเครือข่ายมันคือ replication lag)
linearizable ถูกปฏิเสธบนทุกตัวที่ไม่ใช่ primary การรัน 3-node เชื่อมตรงกับ secondary:
MongoServerError | 10107 NotWritablePrimary | cannot satisfy linearizable read concern on non-primary nodeRead preference: จากที่ไหน
หัวข้อที่มีชื่อว่า “Read preference: จากที่ไหน”read preference เลือกว่า สมาชิกตัวไหน ตอบ primary (ค่าเริ่มต้น) อ่านข้อมูลสดที่สุดเสมอ; secondary/secondaryPreferred กระจายโหลดโดยแลกกับข้อมูลที่ล้าหลัง การรัน 3-node ของ Node driver ทำ explain 20 ครั้งด้วย secondaryPreferred — primary คือ rs-1:
// app/concern-readpref.mjs @skip-verify needs the 3-node replica set (real output below)const client = new MongoClient("mongodb://rs-1:27017,rs-2:27017,rs-3:27017/?replicaSet=rsb3");await client.connect();const members = client.db("lib").collection("members", { readPreference: "secondaryPreferred" });const served = new Set();for (let i = 0; i < 20; i++) served.add((await members.find({}).explain()).serverInfo.host);console.log("reads served by:", [...served].sort());reads served by: [ 'rs-2', 'rs-3' ]และสิ่งที่ secondary แสดงได้: เมื่อ rs-2 ถูกแช่แข็ง (db.fsyncLock()) ขณะที่ primary เขียนสี่ document การอ่านตรงบน rs-2 คืน 1 document ส่วน rs-3 ที่ปกติคืน 4 (output ข้างบน) read concern กับ read preference เป็นอิสระต่อกัน: majority บน secondary ที่ล้าหลังปลอดภัยจาก rollback แต่ก็ยังล้าหลังอยู่
flowchart TD
C["Client sends a write"] --> P["Primary stores it"]
P --> R1["Secondary one replicates"]
P --> R2["Secondary two replicates"]
R1 --> M{"Majority has stored it?"}
R2 --> M
P --> M
M -->|"Yes"| ACK["Acknowledge to client — write is durable"] ใน Compass: dialog ตั้งค่าการเชื่อมต่อให้ตั้ง read preference เริ่มต้นสำหรับทั้ง session ได้ ทำให้การอ่านในแท็บ Documents ถูกส่งไป secondary โดยไม่ต้องแก้ทุก query
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”w: "majority"เป็นค่าเริ่มต้น; ขอw: 1เฉพาะข้อมูลที่ยอมเสียได้ และรู้ว่ามันหายได้ (พิสูจน์ข้างล่าง)wtimeoutจำกัด การรอ ไม่ใช่การเขียน: insertw: 3ที่ timeout ถูกนำไปใช้บน primary แล้ว- การอ่านจาก secondary อาจได้ข้อมูลล้าหลัง เพราะ secondary ตามหลัง primary — ตัวที่ถูกแช่แข็งคืน 1 จาก 4 document
linearizableต้องใช้ primary และควรมีmaxTimeMSไม่เช่นนั้นอาจบล็อกเมื่อเข้าถึง majority ไม่ได้
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ผู้ช่วย AI ที่ “ปรับแต่ง” การเขียน order ลด concern ลงและเขียนคอมเมนต์อย่างมั่นใจ:
🐛 หา bug ในโค้ดนี้
// AI comment: "w: 1 = acknowledged by the primary, which is durable; faster than majority"await orders.insertOne(order, { writeConcern: { w: 1 } });เฉลย
acknowledgement จาก primary ไม่ใช่ความคงทนข้าม failover หลักฐานบน 3-node: primary rs-2 ถูกตัดขาดจากเครือข่าย, insert แบบ w: 1 ยังได้ acknowledge, อีกสองสมาชิกเลือก rs-1 ขึ้นมา และหลังเชื่อม rs-2 กลับ document ที่ได้ acknowledge ไปแล้วถูก rollback และหายไป (BSON ของมันถูกย้ายไป /data/db/rollback/) วิธีแก้คือค่าเริ่มต้น w: "majority" — ระหว่าง partition เดียวกัน มันไม่ acknowledge เท็จ แต่ timeout:
// mongosh/concern-lost-write.js @skip-verify needs the 3-node replica set and a network partition (real output below)const l = db.getSiblingDB("lib");l.lost2.insertOne({ _id: "only-on-old-primary" }, { writeConcern: { w: 1 } }); // acknowledgedtry { l.lost2.insertOne({ _id: "maj" }, { writeConcern: { w: "majority", wtimeout: 3000 } }); }catch (e) { console.log("w:majority ->", e.codeName); }ack on isolated primary: truew:majority -> WriteConcernTimeout(after the partition heals, on the set)rs-1:27017 PRIMARY | rs-2:27017 SECONDARY | rs-3:27017 SECONDARY[ { _id: 'on-new-primary' } ]/data/db/rollback/200fb679-8f77-415c-bafe-7bf7b4b846b3/removed.2026-09-30T08-43-30.1.bsonMental Model
หัวข้อที่มีชื่อว่า “Mental Model”มีสองปุ่มที่เป็นอิสระต่อกัน Write concern บอกว่าต้องมีกี่สำเนามีการเขียนนั้นก่อนที่ คุณ จะถูกบอกว่า “เสร็จ” — มันเปลี่ยนสิ่งที่ผู้เขียนรู้ Read concern บอกว่าข้อมูลต้องนิ่งแค่ไหนก่อนที่ผู้อ่านจะเห็น — มันเปลี่ยนสิ่งที่ผู้อ่านเห็น Read preference แค่เลือก server การเขียนที่ถูกยืนยันโดยน้อยกว่า majority อาจหายไปตอน failover; การอ่านที่ต่ำกว่า majority เห็นมันได้ก่อนที่มันจะหาย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | ประโยชน์ | ต้นทุน |
|---|---|---|
Write concern w: "majority" (ค่าเริ่มต้น) | การเขียนที่ acknowledge แล้วรอด failover | ต้องเสีย network round trip ไป secondary (3 ms เมื่อมีสมาชิกถูกแช่แข็งใน lab); บล็อกหรือ timeout ถ้าเข้าถึง majority ไม่ได้ |
Write concern w: 1 | เร็วที่สุด acknowledge ทันทีที่ primary รับ | พิสูจน์ข้างบน: การเขียนที่ acknowledge แล้วถูก rollback หลัง partition |
Read concern majority / snapshot | ไม่อ่านข้อมูลที่อาจถูก rollback | ตามหลัง local (และการเขียน w: 1 ของคุณเอง) |
Read concern local | เร็วที่สุด ข้อมูลสดที่สุดบนสมาชิกที่ถูก query | อาจอ่านข้อมูลที่ภายหลังถูก rollback |
Read concern linearizable | สะท้อนทุกการเขียน majority ที่เสร็จแล้ว | ใช้ได้เฉพาะ primary; ช้า; ต้องมี maxTimeMS |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
w: 'majority'ต้องมีj: trueถึงจะปลอดภัย” — เมื่อwriteConcernMajorityJournalDefault: true(ค่าเริ่มต้น แสดงข้างบน) majority write รอ journal บนสมาชิกที่นับอยู่แล้ว - “write concern เริ่มต้นคือ
w: 1” — ตั้งแต่ 5.0 คือmajority;getDefaultRWConcernแสดงให้เห็น - “error
wtimeoutแปลว่าการเขียนล้มเหลว” — การเขียนถูกนำไปใช้บน primary แล้ว (primary has 4 docs); ขาดแค่การยืนยัน - “การเขียนที่ acknowledge แล้วปลอดภัย” — เฉพาะเมื่อ concern ครอบคลุม majority; acknowledgement ของ
w: 1ถูก rollback ไป - “read concern กับ read preference คือปุ่มเดียวกัน” — อันหนึ่งเลือกว่าข้อมูลนิ่งแค่ไหน อีกอันเลือกสมาชิก;
majorityบน secondary ที่ล้าหลังปลอดภัยแต่ก็ยังล้าหลัง
แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่ตรวจแล้วใน shell/Node; ไม่ได้ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส