ข้ามไปยังเนื้อหา

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" }
}
)

ทุกส่วนของการเปลี่ยนแปลงลงพร้อมกัน และไม่มี 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
การ embed ข้อมูลที่เกี่ยวข้องกันใน document เดียวหมายความว่าการเขียน atomic ครั้งเดียวรักษาทุกอย่างให้สอดคล้องกัน

ใน Compass: เปิด document สมาชิกในแท็บ Documents แล้วขยาย nested object activity และ array borrowed การแก้ไขหลายฟิลด์เหล่านี้แล้วคลิก Update จะนำไปใช้เป็นการเขียน atomic ครั้งเดียว เหมือนกับการเรียกผ่าน driver ด้านบน

  • atomicity เป็นแบบต่อ document ไม่ใช่ต่อ operation การ updateMany ครั้งเดียวที่แตะ document หนึ่งพันฉบับคือการเขียน atomic ที่เป็นอิสระต่อกันหนึ่งพันครั้ง ไม่ใช่ขั้นตอน atomic ขั้นตอนเดียวขนาดใหญ่ — reader อีกคนสามารถเห็นบางส่วนที่นำไปใช้แล้วและบางส่วนที่ยังไม่ได้
  • การอ่านไม่ block อยู่กับการเขียนเหล่านี้ reader จะได้ document ก่อนการเขียนหรือหลังการเขียน ไม่มีทางเจอสภาพเขียนค้างครึ่งทาง เพราะไม่มีสภาพเขียนครึ่งทางให้รอ
  • การ embed ไม่ได้ฟรีในทุกกรณี ถ้า document จะโตขึ้นโดยไม่มีขอบเขต — บันทึกกิจกรรมที่มีรายการนับล้าน — การแยกออกไปอาจเป็นทางเลือกที่ถูกต้อง แล้วคุณก็ยอมรับว่าการเปลี่ยนแปลงเหล่านั้นต้องใช้กลยุทธ์ความสอดคล้องที่ต่างออกไป
ตัวเลือกBenefitCost
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 condition

Instagram — เก็บ like count เป็น field ใน post document เดียวกับเนื้อหาโพสต์ การกดไลก์แต่ละครั้งคือ $inc แบบ atomic ครั้งเดียว ไม่ต้องมี session หรือ transaction แม้จะมีคนกดพร้อมกันหลายพันครั้งต่อวินาที

การอัปเดตตั้งค่าสองฟิลด์และ push เข้า array ใน document เดียว reader อีกคนสังเกตเห็นอะไรได้บ้าง?
ทำไม counter แบบ $inc จึงปลอดภัยภายใต้ concurrency ภายใน document เดียว?
บทเรียนการออกแบบโมเดลหลักของ single-document atomicity คืออะไร?
การ updateMany ครั้งเดียวบน document หลายฉบับเป็นขั้นตอน atomic ขั้นตอนเดียวหรือไม่?