Single-document atomicity
การรับประกันที่แข็งแกร่งที่สุดที่ MongoDB ให้คุณก็เป็นการรับประกันที่เงียบที่สุดด้วย เพราะไม่มีต้นทุนและไม่ขออะไรจากคุณเลย ทุกการเขียนไปยัง document หนึ่งฉบับ เกิดขึ้นเป็นขั้นตอนเดียวที่แบ่งแยกไม่ได้ ถ้าการอัปเดตของคุณตั้งค่าสามฟิลด์ เพิ่มค่า counter และ push เข้า array ทั้งหมดในคราวเดียว ทุก reader จะเห็นการเปลี่ยนแปลงเหล่านั้นทั้งหมดหรือไม่เห็นเลย — ไม่มีทางเห็นสภาพที่นำไปใช้ครึ่งทาง และถ้ามีอะไรผิดพลาดระหว่างการเขียน document จะถูกทิ้งไว้ในสภาพเดิมเป๊ะอย่างที่เคยเป็นก่อนหน้า สิ่งนี้เป็นจริงไม่ว่าการเปลี่ยนแปลงจะลงลึกแค่ไหน: nested object และ array ภายใน document เป็นส่วนหนึ่งของหน่วย atomic เดียวกัน
เราจะทำงานกับ members ของห้องสมุดเราต่อไป ลองนึกภาพ document สมาชิกที่รวบรวมทุกอย่างเกี่ยวกับคนคนหนึ่งไว้ด้วยกัน — ค่าปรับ หนังสือที่กำลังยืมอยู่ และบันทึกกิจกรรมเล็ก ๆ:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Ada", "fines": 4, "borrowed": ["Algorithms"], "activity": { "lastVisit": 2025, "visits": 12 }}เขียนครั้งเดียว เปลี่ยนหลายอย่าง ก็ยังคง atomic
หัวข้อที่มีชื่อว่า “เขียนครั้งเดียว เปลี่ยนหลายอย่าง ก็ยังคง atomic”สมมติว่า Ada คืนหนังสือ จ่ายค่าปรับสองหน่วย และมาเยือนห้องสมุด — สามการเปลี่ยนแปลงเชิงตรรกะที่เกิดขึ้นในการมาเยือนครั้งเดียวนี้ เพราะทั้งหมดอยู่ใน document เดียว การอัปเดตครั้งเดียวจึงนำไปใช้พร้อมกัน ไม่มี transaction ให้เปิดและไม่มี session ให้จัดการ ตัว operator ทำงานเอง และการอัปเดตทั้งหมดลงในคราวเดียว:
db.members.updateOne( { name: "Ada" }, { $inc: { fines: -2, "activity.visits": 1 }, $set: { "activity.lastVisit": 2026 }, $pull: { borrowed: "Algorithms" } })await db.collection("members").updateOne( { name: "Ada" }, { $inc: { fines: -2, "activity.visits": 1 }, $set: { "activity.lastVisit": 2026 }, $pull: { borrowed: "Algorithms" } });db.members.update_one( {"name": "Ada"}, { "$inc": {"fines": -2, "activity.visits": 1}, "$set": {"activity.lastVisit": 2026}, "$pull": {"borrowed": "Algorithms"}, },)_, err := coll.UpdateOne( ctx, bson.M{"name": "Ada"}, bson.M{ "$inc": bson.M{"fines": -2, "activity.visits": 1}, "$set": bson.M{"activity.lastVisit": 2026}, "$pull": bson.M{"borrowed": "Algorithms"}, },)if err != nil { return err}coll.update_one( doc! { "name": "Ada" }, doc! { "$inc": { "fines": -2, "activity.visits": 1 }, "$set": { "activity.lastVisit": 2026 }, "$pull": { "borrowed": "Algorithms" } },).await?;ทุกส่วนของการเปลี่ยนแปลงลงพร้อมกัน และไม่มี reader คนใดเคยเห็น document ในสภาพที่ ยกตัวอย่างเช่น ค่าปรับลดลงแล้วแต่หนังสือยังถูกยืมอยู่:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Ada", "fines": 2, "borrowed": [], "activity": { "lastVisit": 2026, "visits": 13 }}ตัว update operator เองก็เป็น atomic $inc อ่านค่าปัจจุบันและบวกเข้าไปเป็นขั้นตอนเดียว ดังนั้นการเพิ่มค่าพร้อมกันสองครั้งจะไม่มีทางทำให้การอัปเดตหายไปจากการที่ทั้งคู่อ่านตัวเลขเริ่มต้นเดียวกัน คุณสมบัตินี้นี่เองคือเหตุผลว่าทำไม counter และยอดคงเหลือจึงควรอยู่ภายใน document เดียว แทนที่จะคำนวณในแอปพลิเคชันของคุณแล้วเขียนกลับ
ออกแบบโมเดลให้ข้อมูลที่เกี่ยวข้องกันอยู่ด้วยกัน
หัวข้อที่มีชื่อว่า “ออกแบบโมเดลให้ข้อมูลที่เกี่ยวข้องกันอยู่ด้วยกัน”บทเรียนเชิงปฏิบัติคือเรื่องการออกแบบโมเดล เมื่อข้อมูลเปลี่ยนแปลงไปด้วยกัน ให้เก็บไว้ด้วยกัน แล้ว atomicity ฟรี ๆ จะครอบให้เอง document สมาชิกของเราทำเช่นนี้อยู่แล้ว: ค่าปรับ การยืม และบันทึกกิจกรรมล้วน embed อยู่ ดังนั้นสถานะทั้งหมดของสมาชิกหนึ่งคนจึงเคลื่อนไปเป็นหน่วยเดียว หากเราแยกบันทึกกิจกรรมออกไปเป็น collection activity แยกต่างหากที่ key ด้วยสมาชิก การมาเยือนครั้งเดียวกันนั้นจะคร่อมสอง document และบังคับให้เราต้องเลือกระหว่างการยอมเสีย atomicity หรือจ่ายค่า transaction
นี่คือหัวใจของเหตุผลที่ document model กับ consistency model ของ MongoDB ถึงเข้ากันได้ดี นิสัยแบบ relational ที่ normalise ทุกอย่างลงในตารางแยกกันสร้างการอัปเดตหลายแถวที่ต้องใช้ transaction เพื่อให้สอดคล้องกัน ส่วนนิสัยแบบ document ที่ embed สิ่งที่เป็นของคู่กันเข้าด้วยกันเปลี่ยนหลาย ๆ กรณีเหล่านั้นให้เป็นการเขียน atomic ครั้งเดียว
flowchart LR A["One member document"] --> B["fines"] A --> C["borrowed array"] A --> D["activity object"] B --> E["A single update changes all three atomically"] C --> E D --> E
ใน Compass: เปิด document สมาชิกในแท็บ Documents แล้วขยาย nested object activity และ array borrowed การแก้ไขหลายฟิลด์เหล่านี้แล้วคลิก Update จะนำไปใช้เป็นการเขียน atomic ครั้งเดียว เหมือนกับการเรียกผ่าน driver ด้านบน
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- atomicity เป็นแบบต่อ document ไม่ใช่ต่อ operation การ
updateManyครั้งเดียวที่แตะ document หนึ่งพันฉบับคือการเขียน atomic ที่เป็นอิสระต่อกันหนึ่งพันครั้ง ไม่ใช่ขั้นตอน atomic ขั้นตอนเดียวขนาดใหญ่ — reader อีกคนสามารถเห็นบางส่วนที่นำไปใช้แล้วและบางส่วนที่ยังไม่ได้ - การอ่านไม่ block อยู่กับการเขียนเหล่านี้ reader จะได้ document ก่อนการเขียนหรือหลังการเขียน ไม่มีทางเจอสภาพเขียนค้างครึ่งทาง เพราะไม่มีสภาพเขียนครึ่งทางให้รอ
- การ embed ไม่ได้ฟรีในทุกกรณี ถ้า document จะโตขึ้นโดยไม่มีขอบเขต — บันทึกกิจกรรมที่มีรายการนับล้าน — การแยกออกไปอาจเป็นทางเลือกที่ถูกต้อง แล้วคุณก็ยอมรับว่าการเปลี่ยนแปลงเหล่านั้นต้องใช้กลยุทธ์ความสอดคล้องที่ต่างออกไป
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| embed ข้อมูลไว้ใน document เดียว | atomicity ฟรี ไม่มี session ไม่มี commit latency ต่ำสุดเท่าที่เป็นไปได้ | document โตขึ้นเรื่อย ๆ ตามข้อมูลที่ embed อาจเข้าใกล้ 16MB BSON limit หรือทำให้การอ่าน/เขียนช้าลงเมื่อต้องย้ายทั้งก้อน |
| แยกข้อมูลออกเป็นหลาย document/collection แล้วใช้ multi-document transaction เมื่อต้องเปลี่ยนร่วมกัน | ยืดหยุ่นในการ query และ scale แต่ละส่วนแยกจากกันได้ | เสีย atomicity ฟรีที่เคยได้ ต้องจ่าย session และ commit overhead ทุกครั้งที่ต้องเปลี่ยนแปลงร่วมกัน |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- คิดว่า read-then-write ที่ทำในแอปพลิเคชันเป็น atomic เพราะจบลงด้วยการเขียน document เดียว — ไม่จริง ช่วงเวลาระหว่างการอ่านค่ามาคำนวณกับการเขียนกลับมี race condition ได้เสมอ ต้องใช้ atomic operator อย่าง
$incหรือfindOneAndUpdateที่ทำ read-modify-write บน server ในขั้นตอนเดียวแทน - มองว่า atomic แปลว่า isolated จาก document อื่นด้วย — atomicity รับประกันแค่ document ของตัวเอง ถ้า business logic ต้องอ่าน document อื่นมาประกอบการตัดสินใจก่อนเขียน ยังมี race กับ document นั้นอยู่เหมือนเดิม นี่เป็นคนละปัญหากับ multi-document consistency ที่ต้องแก้ด้วยเครื่องมือคนละอย่าง
- embed ทุกอย่างเข้าไปโดยไม่ดูอัตราการเติบโตของข้อมูล — เห็นว่า embed ให้ atomicity ฟรีก็ embed array ที่โตไม่มีขอบเขต เช่น log ทุก event ของสมาชิก ทำให้ document ดันเข้าใกล้ขีดจำกัดขนาดและทำให้ทุก operation บน document นั้นช้าลงโดยไม่จำเป็น
💡 ตัวอย่างจากของจริง
Shopify — เก็บ inventory count ไว้เป็น field เดียวใน product document แล้วใช้
$incแบบ atomic ลด/เพิ่ม stock เมื่อมีออเดอร์ แทนที่จะเปิด transaction ทุกครั้ง ทำให้ throughput สูงและไม่มี overselling จาก race conditionInstagram — เก็บ like count เป็น field ใน post document เดียวกับเนื้อหาโพสต์ การกดไลก์แต่ละครั้งคือ
$incแบบ atomic ครั้งเดียว ไม่ต้องมี session หรือ transaction แม้จะมีคนกดพร้อมกันหลายพันครั้งต่อวินาที