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

anomaly ที่​ทำซ้ำ​ได้​ทุก​ครั้ง — สาม​ชนิด​เกิด​ไม่​ได้ หนึ่ง​ชนิด​เกิด​จริง

ปัญหา​จาก​การ​รัน​พร้อม​กัน​มีชื่อเสียง​เรื่อง​การ​โผล่​มา​ครั้ง​เดียว​แล้ว​หาย​ไป

รัน​ซ้ำ​อีก​สิบ​รอบ​ก็​ไม่​เจอ สุดท้าย​ทุก​คน​เลิก​เถียง​กัน​โดย​ไม่มี​ใคร​ถูก และ​ไม่มี​ใคร​แก้​อะไร​เลย

บท​นี้​ทำให้​ปรากฏการณ์​แต่ละ​ชนิด​เกิด​ขึ้น​เหมือน​เดิม​ทุกรอบ​ก่อน เพราะ​สิ่ง​ที่​ทำซ้ำ​ไม่​ได้ ไม่ใช่​หลักฐาน

ผล​ที่​ออก​มา​ไม่​ตรง​กับ​ที่​คน​ส่วน​ใหญ่​คาด สาม​ชนิด​แรก​เกิด​ไม่​ได้​เลย​ใน​เอนจินที่​บท​ที่ 1 สร้าง​ไว้ ส่วน​ชนิด​ที่​สี่ เกิด​จริง​โดย​ไม่มี​ใคร​โยน error ออก​มา​สัก​ตัว

📦 code ของ​บท​นี้

ทุก​บล็อก code ใน​บทเรียน​นี้​ถูก​คัด​มา​จาก scripts/transaction-engine/src/ ทีละ byte และ​ผล​รัน​ทุก​บรรทัด​คัด​มา​จาก expected/02-anomalies-you-can-reproduce.txt ที่​รัน​จริง

รัน​ตาม​เอง​ได้​ด้วย cargo run --quiet -- 02 ที่ scripts/transaction-engine/ ถ้า​แก้​ใน​บทเรียน​อย่าง​เดียว​โดย​ไม่​แก้​ที่​ซอร์ส เทสต์จะ​แดง​ทันที

หัวข้อ​นี้​บอกว่า​เอนจิน​ไม่มี thread และ​ไม่มี​การ​หน่วง​เวลา ลำดับ​ก้าว​จึง​เขียน​ไว้​เป็น code ธรรมดา

วิธี​สาธิต​ปัญหา​แบบ​ที่​พบ​บ่อย​คือ​เปิด​สอง thread แล้ว​หวัง​ให้​มัน​แทรก​กัน​ตรง​จุด​ที่​ต้องการ​พอดี

วิธี​นั้น​ได้​ผล​บ้าง​ไม่​ได้​ผล​บ้าง​ตาม​ภาระ​ของ​เครื่อง สิ่ง​ที่​ได้​กลับ​มา​คือ​เทสต์ที่​แดง​สลับ​เขียว ซึ่ง​ไม่มี​ใคร​เชื่อถือ​มัน​ได้​อีก

เอนจิน​นี้​ไม่มี thread สัก​ตัว​และ​ไม่มี​การ​หน่วง​เวลา​สัก​จุด ลำดับ​ก้าว​ของ transaction ทั้ง​สอง​ตัว ถูก​เขียน​เรียง​ไว้​เป็น​คำ​สั่ง​ธรรมดา​บรรทัด​ต่อ​บรรทัด

นั่น​คือ​เหตุผล​เดียว​ที่​บท​นี้​พิมพ์​ตัวเลข​ลง​หน้า​กระดาษ​ได้ เทสต์ของ​คอร์ส​เทียบ​ผล​รัน​ของ​ทุก​บท กับ​ที่​ตรึง​ไว้​ที​ละ byte และ​รัน​บท​หนึ่ง​ซ้ำ​ห้า​รอบ​เพื่อ​ยืนยัน​ว่า​ได้​ผล​เดิม

ราคา​ที่​จ่าย​คือ scenario ใน​บท​นี้​เป็นการ​สลับ​คิว​ที่​เลือก​ไว้​แล้ว ไม่ใช่​ทุก​ลำดับ​ที่​เป็น​ไป​ได้ มัน​แสดง​ว่า​อะไร​เกิด​ขึ้น​ได้ ไม่​ได้​พิสูจน์​ว่า​อะไร​เกิด​ไม่​ได้

หัวข้อ​นี้​แสดง​ว่า​ค่า 999 อยู่​ใน​ที่​เก็บ​จริง แต่​กฎ​การ​มอง​เห็น​ไม่​ยอม​ให้การ​อ่าน​คืน​มัน​ออก​มา

Dirty ReadDirty Readการ​อ่าน​ค่าที่ transaction อื่น​เขียน​ไว้​แต่​ยัง​ไม่ commit ถ้า​อีก​ฝั่ง rollback ค่าที่​อ่าน​ไป​แล้ว​ก็​กลาย​เป็น​ค่าที่​ไม่​เคย​มี​อยู่​จริง คือ​การ​อ่าน​ค่าที่​คน​อื่น​เขียน​ไว้​แต่​ยัง​ไม่ commit ถ้า​อีก​ฝั่ง​ยกเลิก​ทีหลัง ค่าที่​อ่าน​ไป​แล้ว​ก็​กลาย​เป็น​ค่าที่​ไม่​เคย​มี​อยู่​จริง

scenario ของ​มัน​เขียน​ได้​ใน​สิบ​บรรทัด writer เขียน 999 ทิ้ง​ไว้​โดย​ไม่ commit แล้ว reader เปิด​ขึ้น​มา​ทีหลัง

src/demos.rs · scenario ของ dirty read
// dirty read — เอนจินนี้ทำให้เกิดไม่ได้เลยโดยโครงสร้าง
let mut s = Store::new();
let mut setup = txn::begin(&mut s, Level::Snapshot);
setup.write(&mut s, TOTAL, 100);
setup.commit(&mut s, &[]);
let mut writer = txn::begin(&mut s, Level::Snapshot);
writer.write(&mut s, TOTAL, 999);
let mut reader = txn::begin(&mut s, Level::Snapshot);
let raw = s.versions[TOTAL].last().map(|v| v.value).unwrap_or(0);
let seen = reader.read(&s, TOTAL).unwrap_or(0);

สอง​บรรทัด​สุดท้าย​คือ​ทั้งหมด​ของ​การ​ทดลอง raw ล้วง​เข้าไป​หยิบ version ตัว​ท้าย chain ตรงๆ โดย​ไม่​ผ่าน​กฎ​อะไร​เลย ส่วน seen เรียก read ซึ่ง​เดิน​ผ่าน​กฎ​การ​มอง​เห็น

cargo run --quiet -- 02
== บทที่ 2 anomaly ที่ทำซ้ำได้ทุกครั้ง ==
dirty read
version ล่าสุดในที่เก็บจริง 999
ค่าที่กฎการมองเห็นยอมให้อ่าน 100 → เกิดไม่ได้

เลข 999 อยู่​ใน​ที่​เก็บ​จริง​ตาม​ที่​บรรทัด​แรก​รายงาน การ​เขียน​ของ​เอนจิน​นี้​ลง​ที่​เก็บ​ทันที มัน​ไม่​ได้​รอ commit

สิ่ง​เดียว​ที่​กัน​ไม่​ให้​ใคร​เห็น​มัน​คือ​เงื่อนไขข้อ​แรก​ของ visible ซึ่ง​ตัด​ทุก version ที่​คน​สร้าง​ไม่​อยู่​ใน snapshot ของ​ผู้​อ่าน​ออก​ไป

หมายเลข​ของ writer จะ​เข้าไป​อยู่​ใน snapshot ของ reader ได้​ทาง​เดียว คือ writer ต้อง commit ให้​เสร็จ​ก่อน​ที่ reader จะ​เปิด

จึง​ไม่มี​การ​สลับ​คิว​แบบ​ไหน​ที่​ทำให้ dirty read เกิด​ใน​เอนจิน​นี้​ได้ นี่​เป็น​สมบัติ​ของ​การ​ออกแบบ ไม่ใช่​ผล​ของ scenario ที่​เลือก​มา​ดี

หัวข้อ​นี้​แสดง​ว่าการ​อ่าน​ซ้ำ​ใน transaction เดียว​ได้ 100 ทั้ง​สอง​ครั้ง ส่วนตัว​ที่​เปิด​ทีหลัง​เห็น 500

Non-Repeatable ReadNon-Repeatable Readการ​อ่าน​แถว​เดิม​สอง​ครั้ง​ใน transaction เดียว​แล้ว​ได้​ค่า​ต่าง​กัน เพราะ​มี transaction อื่น commit คั่น​กลาง คือ​การ​อ่าน​แถว​เดิม​สอง​ครั้ง​ใน transaction เดียว​แล้ว​ได้​ค่า​ไม่​เท่า​กัน เพราะ​มี​คน​อื่น commit คั่น​กลาง

การ​สลับ​คิว​เขียนไว้ตรงๆ แบบ​เดิม other แทรก​เข้า​มาระหว่าง​การ​อ่าน​สอง​ครั้ง​ของ long_reader แล้ว fresh เปิด​ขึ้น​มา​เป็น​ตัว​สุดท้าย

src/demos.rs · การ​อ่าน​ซ้ำ​ที่​มี​คน commit คั่น​กลาง
let mut long_reader = txn::begin(&mut s, Level::Snapshot);
let first = long_reader.read(&s, TOTAL).unwrap_or(0);
let mut other = txn::begin(&mut s, Level::Snapshot);
other.write(&mut s, TOTAL, 500);
other.commit(&mut s, &[]);
let second = long_reader.read(&s, TOTAL).unwrap_or(0);
let mut fresh = txn::begin(&mut s, Level::Snapshot);
let after = fresh.read(&s, TOTAL).unwrap_or(0);
cargo run --quiet -- 02
non-repeatable read
อ่านครั้งแรก 100 · มีคน commit 500 คั่น · อ่านซ้ำ 100 → เกิดไม่ได้
transaction ที่เปิดใหม่หลังจากนั้นเห็น 500

บรรทัด​ที่​สอง​คือ​ด่าน​กันตัวเอง​หลอก​ตัวเอง other commit ค่า 500 ไป​แล้ว​จริง เพราะ fresh ที่​เปิด​ทีหลัง​อ่าน​ค่า​นั้น​ได้

สิ่ง​ที่​ทำให้ long_reader ไม่​เห็น​คือ snapshot ของ​มัน ซึ่ง​เป็น สำเนา ของ​ทะเบียน committed ณ ตอน​ที่​มัน​เปิด ไม่ใช่​การ​อ้างอิง​ไป​ที่​ทะเบียน

สำเนา​นั้น​จึง​ไม่มี​หมายเลข​ของ other อยู่​เลย version ที่ other เขียน​ไว้​ก็​ตก​เงื่อนไขข้อ​แรก เหมือน​กับ​ของ writer ใน​หัวข้อ​ที่​แล้ว​ทุก​ประการ

ราคา​ต้อง​พูดตรงๆ ด้วย long_reader อ่าน​ค่าที่​ล้าสมัย​ไป​แล้ว มัน​ได้​ภาพ​ที่​คงที่ ไม่ใช่​ภาพ​ที่​ใหม่​ที่สุด

หัวข้อ​นี้​แสดง​ว่า​แถว​ที่​เพิ่ม​เข้า​มา​ใหม่​อยู่​ใน​ที่​เก็บ​จริง แต่​ไม่​ถูก​นับ เพราะ​ไม่มี version ที่​มอง​เห็น​ได้

Phantom ReadPhantom Readการ​ถาม​ด้วย​เงื่อนไข​เดิม​สอง​ครั้ง​แล้ว​ได้​จำนวน​แถว​ไม่​เท่า​กัน เพราะ​มี​คน​เพิ่ม​หรือ​ลบ​แถว​ที่​เข้า​เงื่อนไข​คั่น​กลาง ต่าง​จาก non-repeatable read ตรง​ที่​จำนวน​แถว​เปลี่ยน ไม่ใช่​ค่า​ใน​แถว​เดิม​เปลี่ยน ต่าง​จาก​ชนิด​ที่​แล้ว​ตรง​ที่​จำนวน​แถว​เปลี่ยน ไม่ใช่​ค่า​ใน​แถว​เดิม​เปลี่ยน คน​ที่​ถาม​ด้วย​เงื่อนไข​เดิม​สอง​ครั้ง​จึง​ได้​จำนวน​ไม่​เท่า​กัน

counter นับ​ไร​เด​อร์ที่​มอง​เห็น​สอง​ครั้ง โดย​มี inserter เพิ่ม​ไร​เด​อร์คน​ที่​สอง​แล้ว commit คั่น​กลาง

src/demos.rs · การ​นับ​ด้วย​เงื่อนไข​เดิม​สอง​ครั้ง
let mut counter = txn::begin(&mut s, Level::Snapshot);
let n1 = counter.read_all(&s).len();
let mut inserter = txn::begin(&mut s, Level::Snapshot);
inserter.write(&mut s, BUNMA, 1);
inserter.commit(&mut s, &[]);
let n2 = counter.read_all(&s).len();
cargo run --quiet -- 02
phantom read
นับครั้งแรก 1 แถว · มีคนเพิ่มไรเดอร์แล้ว commit · นับซ้ำ 1 แถว → เกิดไม่ได้

คีย์​ของ​ไร​เด​อร์คน​ที่​สอง​อยู่​ใน​ที่​เก็บ​แล้ว​จริง read_all วน​ผ่าน​มัน​ทุก​ครั้ง​ที่​ถูก​เรียก

แต่ read_all เรียก read ที​ละ​คีย์ และ​คีย์​ที่​ไม่มี version ไหน​มอง​เห็น​ได้​เลย​จะ​คืน None แล้ว​หาย​ไป​จาก​ผล​ที่​ส่ง​กลับ

จุด​ที่​คุ้ม​กับ​การ​หยุด​คิด​คือ phantom ใน​เอนจิน​นี้​ไม่​ต้องการ​กลไก​เพิ่ม​สัก​ชิ้น กฎ​เดียว​กับ​ที่​กัน dirty read กัน​มัน​ไป​ด้วย​แล้ว

ใน​เอนจินที่​คุม​ด้วย lock ปัญหา​นี้​ยาก​กว่า​นั้น​มาก เพราะ​แถว​ที่​ยัง​ไม่มี​อยู่​ก็​ยัง​ไม่มี​อะไร​ให้ lock

หัวข้อ​นี้​แสดง​ว่า​ทั้ง​สอง transaction commit ผ่าน แล้ว​ยอด​สุดท้าย​หาย​ไป 50 จาก​คำ​ตอบ​ของ​การ​รัน​เรียง​ที​ละ​ตัว

Lost UpdateLost Updateการ​ที่​การ​แก้​ค่า​ของ transaction หนึ่ง​ถูก​เขียน​ทับ​หาย​ไป เพราะ​อีก​ตัว​อ่าน​ค่า​เดิม​ไป​ก่อน​แล้ว​เขียน​ทับ​ทีหลัง ผล​รวม​ที่​ได้​จึง​น้อย​กว่า​ที่​ควร​เป็น คือ​การ​ที่​การ​แก้​ค่า​ของ transaction หนึ่ง​หาย​ไป เพราะ​อีก​ตัว​อ่าน​ค่า​เดิม​ไป​ก่อน แล้ว​เขียน​ทีหลัง

โจทย์​คือ​สอง​คน​บวก​ยอด​คนละ​ก้อน​บน​ยอด​ตั้งต้น 100 คน​แรก​บวก 50 คน​ที่​สอง​บวก 30

src/demos.rs · สอง​คน​บวก​ยอด​คนละ​ก้อน​บน​ภาพ​เดียวกัน
/// สองคนอ่านยอด 100 แล้วบวกคนละก้อน · ถ้ารันเรียงทีละตัวต้องได้ 180
pub const SERIAL_ANSWER: i64 = 180;
pub fn lost_update(level: Level) -> Lost {
let mut s = Store::new();
let mut setup = txn::begin(&mut s, Level::Snapshot);
setup.write(&mut s, TOTAL, 100);
setup.commit(&mut s, &[]);
let mut t1 = txn::begin(&mut s, level);
let mut t2 = txn::begin(&mut s, level);
let v1 = t1.read(&s, TOTAL).unwrap_or(0);
let v2 = t2.read(&s, TOTAL).unwrap_or(0);
t1.write(&mut s, TOTAL, v1 + 50);
t2.write(&mut s, TOTAL, v2 + 30);
let ok1 = t1.commit(&mut s, &[&t2]);
let ok2 = t2.commit(&mut s, &[&t1]);

ทั้ง​คู่​เปิด​ก่อน​ที่​อีก​ฝ่าย​จะ commit ทั้ง​คู่​จึง​อ่าน​ได้ 100 เท่า​กัน แล้ว​ต่าง​คน​ต่าง​คำนวณ จาก​เลข​ตัว​เดียวกัน​นั้น

cargo run --quiet -- 02
lost update
ทั้งคู่ commit ได้ ["t1", "t2"]
ยอดสุดท้าย 130 · คำตอบถ้ารันเรียงทีละตัว 180
หายไป 50 → **เกิดจริง**

180 คือ​คำ​ตอบ​ที่​ควร​ได้ ไม่​ว่า​เอนจิน​จะ​รัน​สอง​ตัว​นี้​เรียง​ที​ละ​ตัว​ใน​ลำดับ​ไหน​ก็ตาม

ผล​จริง​คือ 130 ไม่มี error สัก​ตัว ไม่มี​ใคร​ถูก abort และ​ทั้ง​คู่​รายงาน​กลับ​มา​ว่า​สำเร็จ

การ​เขียน​ของ t1 ไม่​ได้​ถูกลบ​ทิ้ง​ไป​ไหน version ที่​มี​ค่า 150 ยัง​อยู่​ใน chain ตลอด

แต่ read ไล่ chain จาก​ท้าย​มา​หน้า แล้ว​หยุด​ที่​ตัว​ใหม่​สุด​ที่​มอง​เห็น​ได้ ซึ่ง​คือ version ของ t2 ที่​มี​ค่า 130

flowchart TD
  S["setup · xid 1<br/>ยอด 100 แล้ว commit"]
  T1["t1 · xid 2<br/>อ่าน 100 แล้วเขียน 150"]
  T2["t2 · xid 3<br/>อ่าน 100 แล้วเขียน 130"]
  CH["chain ของ order:total<br/>100 → 150 → 130"]
  R["ผู้อ่านคนถัดไปได้ 130<br/>คำตอบถ้ารันเรียงทีละตัว 180"]
  S --> T1
  S --> T2
  T1 -->|commit ผ่าน| CH
  T2 -->|commit ผ่าน ต่อท้าย chain| CH
  CH ==>|หยุดที่ตัวใหม่สุดที่มองเห็นได้| R

คำ​บรรยาย​ภาพ: ทั้ง​คู่​ตัดสิน​ใจ​บน​เลข 100 ตัว​เดียวกัน version ที่ t1 เขียน​ยัง​อยู่​ใน chain แต่​ผู้​อ่าน​คน​ถัด​ไป​หยุด​ที่​ตัว​ท้าย​สุด จึง​ได้ 130

50 บาท​หาย​ไป​เงียบๆ และ​ระบบ​ก็​เดิน​ต่อ​ไป​บน​เลข 130 นั้น​เหมือน​ไม่มี​อะไร​เกิด​ขึ้น

🔁 ทำไม​ตำรา​ถึง​บอกว่า snapshot isolation กัน lost update ได้

Berenson และ​คณะ​นิยาม snapshot isolation ไว้​พร้อม​ด่าน​หนึ่ง​ชื่อ first-committer-wins ตัว​ที่ commit ทีหลัง​จะ​ถูก abort ถ้า​มัน​เขียน​คีย์​ที่​คน​อื่น commit ไป​แล้ว​ใน​ช่วง​เวลา​ที่​ทับ​กัน บทความ​นั้น​ระ​บุตรงๆ ว่า​ด่าน​นี้​ตัด lost update ทิ้ง​ตั้งแต่​ต้น

commit ของ​เรา​ที่​ระดับ Level::Snapshot ยัง​ไม่​ตรวจ​อะไร​เลย​สัก​ข้อ เอนจินตอน​นี้​จึง​ให้ ภาพ​การ​อ่าน​แบบ snapshot มา​แล้ว แต่​ยัง​ไม่มี​ด่าน​นั้น

นี่​เป็น​ความ​ตั้งใจ ไม่ใช่​ของ​ที่​ลืม บท​นี้​ต้องการ lost update ที่​เกิด​จริง​ให้​ดู​ก่อน แล้ว​บท​ที่ 6 จะ​ติด​ด่าน​เข้าไป​แล้ว​วัด​ว่า​ได้​อะไร​กลับ​มา คำ​ตอบ​ไม่ใช่ 180

หัวข้อ​นี้​เทียบ​ผล​รัน​กับ​ตาราง​สาม​ช่อง​ของ ANSI แล้ว​ชี้​ว่า​ป้าย​ที่​ตาราง​ให้ ไม่​ตรง​กับ​สิ่ง​ที่​วัด​ได้

ANSI SQL-92 นิยาม isolation level ไว้​สี่​ระดับ​ด้วย​ตาราง​เดียว แต่ละ​ระดับ​คือ​รายชื่อ ปรากฏการณ์​ที่​ระดับ​นั้น​ห้าม​ไม่​ให้​เกิด

ระดับdirty readnon-repeatable readphantom read
READ UNCOMMITTEDเกิด​ได้เกิด​ได้เกิด​ได้
READ COMMITTEDห้ามเกิด​ได้เกิด​ได้
REPEATABLE READห้ามห้ามเกิด​ได้
SERIALIZABLEห้ามห้ามห้าม

เอนจินของ​เรา​ห้าม​ครบ​ทั้ง​สาม​ช่อง ตาม​ที่​ผล​รัน​สาม​หัวข้อ​แรก​แสดง​ไว้ ถ้า​ใช้​ตาราง​นี้​เป็น​เกณฑ์ ตัดสิน มัน​จะ​ได้​ป้าย SERIALIZABLE

แต่ 130 ไม่​เท่ากับ 180 ผล​ของ​การ​รัน​พร้อม​กัน​จึง​ไม่​ตรง​กับ​ผล​ของ​การ​รัน​เรียง​ที​ละ​ตัว​ลำดับ​ไหน​เลย ตาม​นิยาม​แล้ว​มัน​ไม่ serializable

Berenson และ​คณะ​เขียน​เรื่อง​นี้​ไว้​ตั้งแต่​ปี 1995 พวก​เขา​เรียก​ประวัติ​ที่​ห้าม​ครบ​สาม​ช่อง แต่​ยัง​ไม่ serializable ว่า ANOMALY SERIALIZABLE ซึ่ง​ตรง​กับ​สภาพ​ของ​เอนจิน​เรา​พอดี

บทความ​เดียวกัน​ชี้​ด้วย​ว่า​ทำไม​คน​ถึง​เข้าใจ​ผิด​กัน​ทั้ง​วงการ ตัว​มาตรฐาน ANSI เอง​มี​ข้อ​กำกับ อยู่​ว่า​ระดับ SERIALIZABLE ต้อง​ให้​ผล​ที่ serializable จริง​ด้วย

ข้อ​กำกับ​นั้น​เสียง​เบา​กว่า​ตาราง คน​จึง​อ่าน​ตาราง​แล้ว​สรุป​ว่า​ห้าม​ครบ​สาม​ช่อง​เท่ากับ serializable ซึ่ง​ไม่​จริง

lost update ไม่​ได้​อยู่​ใน​ตาราง​นั้น​เลย​สัก​ช่อง มัน​เป็น​ปรากฏการณ์​ที่​บทความ​นั้น​เพิ่ม​เข้า​มา​เอง ใน​ชื่อ P4

นี่​คือ​เหตุผล​ที่​บทความ​ชื่อ A Critique of ANSI SQL Isolation Levels มี​อยู่​บน​โลก

หัวข้อ​นี้​บอกว่า​บท​ที่ 3 จะ​แก้​ปัญหา​ที่​บท​นี้​พบ​ด้วย lock และ​จะ​นับ​ราคา​ของ​มัน

ตอน​นี้​เรา​มี​ปัญหา​หนึ่ง​ข้อ​ที่​ทำซ้ำ​ได้​ทุก​ครั้ง และ​มี​เลข​สอง​ตัว​ที่​ใช้​เถียง​กัน​ได้ คือ 130 กับ 180

ทาง​แก้​ที่​เก่า​ที่สุด​และ​ตรง​ที่สุด​คือ​ไม่​ให้​ใคร​แตะ​คีย์​เดียวกัน​พร้อม​กัน ใคร​มา​ก่อน​ได้​ไป​ก่อน คน​ที่มา​ทีหลัง​รอ

บท​ที่ 3 จะ​สร้าง lock manager ตัว​นั้น​ขึ้น​มา​จริง แล้ว​นับ​ว่า​มี​กี่​ก้าว​ที่​ถูก​บล็อก และ​เกิด deadlock กี่​ครั้ง

การ​รอ​ไม่ใช่​ของ​ฟรี และ​สอง​ตัวเลข​นั้น​คือ​ราคา​ที่​นับ​ได้ ไม่ใช่​ความ​รู้สึก


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • A Critique of ANSI SQL Isolation Levels — Berenson, Bernstein, Gray, Melton, O’Neil, O’Neil (ACM SIGMOD 1995 หน้า 1–10, รายงาน MSR-TR-95-51 ของ Microsoft Research — ตรวจ​แล้ว 2026-08-13) ต้นทาง​ของ P4 (Lost Update) ซึ่ง​ไม่มี​อยู่​ใน​ตาราง ANSI, ของ​คำ​ว่า ANOMALY SERIALIZABLE และ​ของ​ข้อสังเกต​ว่า​มาตรฐาน ANSI ไม่​ได้​นิยาม SERIALIZABLE ด้วย​ตาราง​นั้น​อย่าง​เดียว
  • PostgreSQL 18 Documentation — 13.2 Transaction Isolation (ตรวจ​แล้ว 2026-08-13) — ตาราง​ที่ 13.1 ระบุ​ว่า​ปรากฏการณ์​ชนิด​ไหน​เกิด​ได้ที่​ระดับ​ไหน และ​มี​ประโยค​ที่​ยืนยัน​ว่า Repeatable Read ของ PostgreSQL สร้าง​ด้วย​เทคนิค​ที่​วงการ​เรียก​ว่า snapshot isolation ซึ่ง​บท​ที่ 5 จะ​ไป​ถึง

เช็กความเข้าใจ — บทที่ 2

ข้อ 1 / 3

ผลรันบอกว่า version ล่าสุดในที่เก็บจริงคือ 999 แต่ค่าที่กฎการมองเห็นยอมให้อ่านคือ 100 ข้อใดอธิบายได้ถูกต้องว่าทำไม dirty read จึงเกิดในเอนจินนี้ไม่ได้