transaction สัญญาอะไร — และไม่ได้สัญญาอะไร
ระบบที่มีผู้ใช้คนเดียวไม่ต้องการอะไรในคอร์สนี้เลยสักบรรทัด
อ่านค่า คิด เขียนกลับ ไม่มีใครมาแทรกกลางทาง ผลที่ได้คือผลที่ควรได้เสมอ
ปัญหาทั้งหมดเริ่มตอนมีคนที่สอง และสิ่งที่ทำให้มันร้ายกว่าที่ควรคือ มันไม่ส่งเสียง ยอดที่ผิดไม่ได้โยน error ออกมา มันแค่ผิด แล้วระบบก็เดินต่อไปบนตัวเลขนั้น
ทั้งคอร์สเขียนด้วย Rust standard library ล้วน ไม่มี external crate สักตัว
ตรวจได้จาก scripts/transaction-engine/Cargo.toml ที่ไม่มีหัวข้อ [dependencies] อยู่เลย
ทุกบล็อก code ในบทเรียนนี้ถูกคัดมาจาก scripts/transaction-engine/src/ ทีละ byte
และผลรันทุกบรรทัดคัดมาจาก expected/ ที่รันจริง ถ้าแก้ในบทเรียนอย่างเดียวโดยไม่แก้ที่ซอร์ส
เทสต์จะแดงทันที
คำสัญญาสองข้อที่คนมักรวมเป็นก้อนเดียว
หัวข้อที่มีชื่อว่า “คำสัญญาสองข้อที่คนมักรวมเป็นก้อนเดียว”หัวข้อนี้แยก atomicity กับ isolation ออกจากกัน และแสดงว่าระบบที่มี atomicity ครบยังตอบผิดได้
TransactionTransactionกลุ่มคำสั่งที่ฐานข้อมูลปฏิบัติต่อมันเหมือนเป็นคำสั่งเดียว — สำเร็จทั้งก้อนหรือไม่เกิดอะไรขึ้นเลย ไม่มีสภาพครึ่งๆ กลางๆ ให้ใครเห็น มักถูกอธิบายด้วยประโยคเดียวว่า “สำเร็จทั้งหมดหรือไม่เกิดอะไรเลย” ประโยคนั้นถูก แต่มันตอบแค่ครึ่งเดียวของเรื่อง
AtomicityAtomicityคำสัญญาว่า transaction จะ commit ทั้งก้อนหรือ rollback ทั้งก้อน เป็นคนละเรื่องกับ isolation ซึ่งตอบว่าระหว่างที่มันยังไม่จบ คนอื่นเห็นอะไรบ้าง ตอบคำถามเกี่ยวกับ ตัวฉันเอง — งานของฉันจะลงครบทุกชิ้นหรือไม่ลงเลย มันไม่ได้พูดถึงคนอื่นสักคำ
คำถามที่เหลือคือ ระหว่างที่งานของฉันยังไม่จบ คนอื่นเห็นอะไรบ้าง และคำถามนั้น atomicity ไม่ตอบให้เลย
นั่นคืองานของ isolation และมันเป็นคนละกลไกกันคนละตัว ระบบที่มี atomicity ครบถ้วน ยังให้คำตอบผิดได้ทุกวัน ถ้า isolation ของมันหลวม
ที่เก็บที่ไม่เคยเขียนทับของเดิม
หัวข้อที่มีชื่อว่า “ที่เก็บที่ไม่เคยเขียนทับของเดิม”หัวข้อนี้แสดงว่าการแก้ค่าในเอนจินนี้คือการต่อของใหม่ ไม่ใช่การทับของเก่า
ถ้าเขียนทับค่าเดิมทิ้งไปตรงๆ transaction ที่เปิดค้างอยู่จะไม่มีทางอ่านภาพเดิมได้อีก เพราะภาพเดิมหายไปแล้วจริงๆ
เอนจินนี้จึงไม่เขียนทับสักครั้ง มันเก็บทุกค่าที่เคยมีไว้ทั้งหมด แล้วติดป้ายกำกับว่า ใครสร้างและใครปิด
นี่ไม่ใช่ทางเดียวที่ทำได้ Oracle กับ InnoDB เขียนทับที่เดิมแล้วประกอบภาพเก่าคืนจาก undo log แทน คอร์สนี้เลือกทางที่ต่อของใหม่ เพราะทุกอย่างที่เกิดขึ้นเห็นได้ด้วยตา
/// หนึ่ง version ของหนึ่งคีย์////// `begin` คือ transaction ที่สร้างมันขึ้นมา ส่วน `end` คือ transaction ที่ปิดมัน/// `end == None` แปลว่า version นี้ยังเป็นตัวล่าสุดอยู่#[derive(Debug, Clone)]pub struct Version { pub begin: Xid, pub end: Option<Xid>, pub value: Value,}สองฟิลด์นี้พอสำหรับทุกอย่างที่เหลือในคอร์ส การแก้ค่าคือการปิด version เดิมแล้วต่อตัวใหม่ ส่วนการลบคือการปิดโดยไม่ต่ออะไรเลย
กฎสองข้อที่ตัดสินว่าใครเห็นอะไร
หัวข้อที่มีชื่อว่า “กฎสองข้อที่ตัดสินว่าใครเห็นอะไร”หัวข้อนี้แสดงกฎการมองเห็น ซึ่งเป็นสิ่งเดียวที่กันไม่ให้ใครอ่านค่าที่ยังไม่ commit
เมื่อของเก่าไม่ถูกลบทิ้ง คำถามที่ตามมาทันทีคือ แล้วใครควรเห็น version ไหน
คำตอบอยู่ใน function เดียว และมันสั้นกว่าที่คนส่วนใหญ่คาด
pub fn visible(v: &Version, snapshot: &BTreeSet<Xid>, me: Xid) -> bool { if v.begin != me && !snapshot.contains(&v.begin) { return false; } match v.end { Some(end) => end != me && !snapshot.contains(&end), None => true, }}snapshot คือสำเนาของทะเบียนว่า ตอนที่ฉันเปิด มีใคร commit ไปแล้วบ้าง มันเป็นสำเนา
ไม่ใช่การอ้างอิง จึงไม่ขยับตามใครอีกเลยตลอดอายุของ transaction
กฎสองข้อที่เหลืออ่านได้ตรงๆ จาก code
- ข้อแรก ถ้าคนสร้าง version นี้ยังไม่ commit ก่อนที่ฉันจะเปิด ฉันไม่เห็นมัน ยกเว้นคนสร้างคือฉันเอง เพราะทุกคนคาดหวังว่าเขียนแล้วอ่านกลับมาต้องเจอ
- ข้อที่สอง ถ้าคนที่ปิดมัน commit ไปก่อนฉันเปิดแล้ว ฉันก็ไม่เห็นมันเช่นกัน
ข้อที่สองดูเหมือนของแถม แต่บทที่ 4 จะแสดงว่ามันคือสิ่งเดียวที่ทำให้การลบทำงานได้
เอนจินนี้ไม่มี abort() ให้เรียก การยกเลิกคือการที่หมายเลขของ transaction นั้น
ไม่เคยเข้าไปอยู่ในทะเบียน committed เท่านั้น
version ที่มันทิ้งไว้จึงตกกฎข้อแรกตลอดกาล โดยไม่ต้องมีใครไปตามลบให้ การยกเลิก จึงไม่ต้องเขียนข้อมูลกลับเป็นค่าเดิมสักแถวเดียว
แต่มันไม่ได้ฟรีทั้งหมด หมายเลขของตัวที่ยกเลิกยังค้างอยู่ในช่อง end ของ version เดิม
คนที่มาทีหลังจึงต้องเปิดทะเบียนดูทุกครั้งว่าหมายเลขนั้นนับหรือไม่นับ นี่คือหน้าที่ของ
สิ่งที่ PostgreSQL เรียกว่า commit log กับ hint bit
flowchart TD S["setup · xid 1<br/>เขียน 100 แล้ว commit"] G["ghost · xid 2<br/>เขียน 999 ไม่ commit"] R["reader · xid 3<br/>snapshot มีแค่ xid 1"] V1["version · begin 1 · end 2<br/>value 100"] V2["version · begin 2 · ยังเปิดอยู่<br/>value 999"] S --> V1 G --> V2 G -.->|ปิด version เดิม| V1 R ==>|เห็น| V1 R -.->|ไม่เห็น เพราะ xid 2 ไม่อยู่ใน snapshot| V2
คำบรรยายภาพ: ghost ปิด version เดิมไปแล้ว แต่ reader ยังเห็นค่า 100 อยู่ เพราะคนที่ปิดมันยังไม่ commit
รันดูเอง แล้วดูตัวเลขที่สำคัญที่สุด
หัวข้อที่มีชื่อว่า “รันดูเอง แล้วดูตัวเลขที่สำคัญที่สุด”หัวข้อนี้แสดงผลรันจริง และชี้ว่าบรรทัดไหนคือประเด็นของทั้งบท
== บทที่ 1 transaction สัญญาอะไร == หลัง setup commit ยอด = 100 · version ที่ค้าง = 1 ghost เขียน 999 แล้วไม่ commit คนอื่นอ่านได้ 100 version ที่ค้างในที่เก็บ 2 transaction ที่ commit แล้ว 1สองบรรทัดกลางคือสิ่งที่คาดไว้ ghost เขียน 999 ลงไปจริง และไม่มีใครเห็นมันเลย
บรรทัดที่ควรหยุดอ่านคือบรรทัดถัดมา version ที่ค้างในที่เก็บมีสองตัว ทั้งที่มี transaction commit สำเร็จแค่ตัวเดียว
ค่าที่ไม่มีใครมองเห็นนั้นไม่ได้หายไปไหน มันยังกินที่อยู่จริง สิ่งที่กันไม่ให้ใครเห็นมันคือ กฎการมองเห็น ไม่ใช่การลบทิ้ง
นี่คือเหตุผลที่ฐานข้อมูลซึ่งเก็บหลาย version ต้องมี garbage collection คอยกวาดของที่
ไม่มีใครใช้แล้วออกไป ใน PostgreSQL งานนี้คือคำสั่ง VACUUM
มันคือราคาที่วิธีนี้จ่ายเพื่อแลกกับการที่ผู้อ่านไม่ต้องรอผู้เขียน เอนจินที่เลือกจ่ายด้วย วิธีอื่นก็มี บทที่ 8 จะแสดงว่า SQLite เลือกทางที่ไม่ต้องเก็บกวาดแบบนี้เลย
แล้ว isolation level อยู่ตรงไหนของเรื่องนี้
หัวข้อที่มีชื่อว่า “แล้ว isolation level อยู่ตรงไหนของเรื่องนี้”หัวข้อนี้บอกว่าคำที่เหลือของคอร์สจะเข้ามาเมื่อไร และบทนี้ยังไม่ตอบอะไรเกี่ยวกับมัน
ทุกอย่างในบทนี้ใช้กติกาชุดเดียว คือทุกคนอ่านจาก snapshot ของตัวเอง
Isolation LevelIsolation Levelระดับที่กำหนดว่า transaction ที่รันพร้อมกันจะกันกันแค่ไหน ระดับที่สูงขึ้นไม่ได้แปลว่าคำตอบถูกขึ้น แต่แปลว่าเอนจินยอมปฏิเสธงานบ่อยขึ้น คือชื่อของกติกาที่เลือกได้ว่าจะเข้มแค่ไหน และคำถามที่ทั้งคอร์สจะตอบคือ ระดับที่ต่างกัน ซื้ออะไร และ จ่ายด้วยอะไร
คำตอบที่คนส่วนใหญ่คาดไว้คือระดับที่สูงขึ้นให้คำตอบที่ถูกต้องกว่า คำตอบนั้นไม่ตรงกับที่วัดได้ และบทที่ 6 จะแสดงตัวเลขที่ขัดกับมันตรงๆ
บทที่ 2 จะเริ่มจากการทำให้ความผิดพลาดแต่ละชนิดเกิดขึ้นซ้ำได้ทุกครั้งก่อน เพราะสิ่งที่ทำซ้ำ ไม่ได้ ก็เถียงกันด้วยตัวเลขไม่ได้
- A Critique of ANSI SQL Isolation Levels — Berenson, Bernstein, Gray, Melton, O’Neil, O’Neil (ACM SIGMOD 1995, รายงาน MSR-TR-95-51 ของ Microsoft Research — ตรวจแล้ว 2026-08-13) ต้นทางของการแยกปรากฏการณ์แต่ละชนิดออกจากกันอย่างชัดเจน และเป็นที่มาของนิยาม snapshot isolation ที่คอร์สนี้จะไปถึงในบทที่ 5
- PostgreSQL 18 Documentation — 13.2 Transaction Isolation (ตรวจแล้ว 2026-08-13) — ตารางที่ระบุว่าปรากฏการณ์ชนิดไหนเกิดได้ที่ระดับไหน ซึ่งบทที่ 8 จะเอามาเทียบกับผลรันของเอนจินที่เราสร้างเอง
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3ระบบหนึ่งรับประกัน atomicity ครบถ้วน ทุก transaction ลงครบทุกชิ้นหรือไม่ลงเลย ข้อใดอธิบายได้ถูกต้องว่าระบบนี้ยังให้คำตอบผิดได้อย่างไร