- ธุรกรรม คือโครงสร้างสำหรับรันงานหลายอย่างในฐานข้อมูลเป็น หน่วยอะตอมมิก เดียว โดยรวมถึงการอ่าน การเขียน การอัปเดต และการลบ
- MySQL และ Postgres ควบคุมธุรกรรมด้วย
begin; และ commit; และยกเลิกการเปลี่ยนแปลงด้วย rollback; เมื่อเกิดความล้มเหลวหรือข้อผิดพลาด
- ฐานข้อมูลทั้งสองต่างรับประกัน การอ่านแบบสอดคล้องกัน (consistent read) แต่ Postgres ใช้ การเก็บหลายเวอร์ชันของแถว (MVCC) ส่วน MySQL ใช้ undo log
- ระดับการแยก (isolation level) ใช้ควบคุมการรบกวนกันของข้อมูลระหว่างธุรกรรม โดยแบ่งเป็น 4 ระดับตั้งแต่ Serializable ถึง Read Uncommitted
- Postgres และ MySQL จัดการกับ ความขัดแย้งจากการเขียนพร้อมกัน ด้วยวิธีที่ต่างกัน โดย Postgres ใช้การตรวจสอบแบบมองโลกในแง่ดี ส่วน MySQL ใช้ การล็อกระดับแถว (row-level locking)
แนวคิดพื้นฐานของธุรกรรม
- ธุรกรรมคือโครงสร้างที่รวมงาน SQL หลายรายการในฐานข้อมูลให้เป็น หน่วยการทำงานแบบอะตอมมิก เดียว
- เริ่มต้นด้วย
begin; และสิ้นสุดด้วย commit; โดยสามารถรันหลายคิวรีระหว่างนั้นได้
- เมื่อ
commit; การเปลี่ยนแปลงทั้งหมดจะถูกนำไปใช้พร้อมกันในครั้งเดียว
- หากเกิดความขัดข้องที่ไม่คาดคิด (เช่น ไฟดับ ดิสก์เสีย เป็นต้น) หรือมีการยกเลิกโดยตั้งใจ สามารถย้อนการเปลี่ยนแปลงด้วย
rollback;
- Postgres รองรับการกู้คืนด้วย WAL (Write-Ahead Log)
- ข้อมูลที่ถูกเปลี่ยนระหว่างธุรกรรมจะถูก แยกออกจากกัน จนมองไม่เห็นจากเซสชันอื่น
- เมื่อ
rollback; การเปลี่ยนแปลงทั้งหมดจะถูกยกเลิก และฐานข้อมูลจะกลับสู่สถานะเดิม
การอ่านแบบสอดคล้องกัน (Consistent Reads)
- ธุรกรรมต้องรักษา มุมมองข้อมูลที่สอดคล้องกัน ซึ่งไม่ได้รับผลกระทบจากการเปลี่ยนแปลงภายนอกระหว่างการทำงาน
- MySQL และ Postgres รองรับสิ่งนี้ตั้งแต่โหมด
REPEATABLE READ ขึ้นไป แต่มีวิธีการทำงานต่างกัน
- Postgres: จัดการเวอร์ชันของแต่ละแถวด้วย การเก็บหลายเวอร์ชันของแถว (MVCC)
- MySQL: ใช้ undo log เพื่อสร้างเวอร์ชันย้อนหลังขึ้นมาใหม่
การเก็บหลายเวอร์ชันของแถวใน Postgres
- ทุกครั้งที่มีการอัปเดตแถว จะมีการสร้างเวอร์ชันใหม่ โดยเวอร์ชันเก่าจะบันทึก transaction ID ใน
xmax และเวอร์ชันใหม่ใน xmin
- ก่อนที่ธุรกรรมจะคอมมิต เซสชันอื่นจะไม่สามารถเห็นการเปลี่ยนแปลงนั้นได้
- หลังคอมมิต เวอร์ชันใหม่จะถูกสะท้อนไปยังทั้งฐานข้อมูล
- เมื่อ
rollback; การเปลี่ยนแปลงจะถูกทิ้งและข้อมูลเดิมยังคงอยู่
- เวอร์ชันแถวเก่าจะถูกจัดการด้วยคำสั่ง
VACUUM FULL เพื่อกู้คืนพื้นที่จัดเก็บ
Undo Log ของ MySQL
- MySQL เขียนทับแถวโดยตรง แต่จะบันทึกค่าก่อนหน้าไว้ใน undo log เพื่อให้กู้คืนได้เมื่อจำเป็น
- แต่ละแถวมีเมตาดาตาเป็น
xid (transaction ID ของการแก้ไขล่าสุด) และ ptr (พอยน์เตอร์ไปยัง undo log)
- เมื่อมีหลายธุรกรรมทำงานพร้อมกัน แต่ละธุรกรรมจะเลือกอ่านเวอร์ชันที่ต้องการได้ผ่าน undo log
- อาจมีบันทึก undo log หลายรายการสำหรับแถวเดียวกัน และจะเลือกเวอร์ชันที่เหมาะสมตาม transaction ID
ระดับการแยก (Isolation Levels)
- เป็นการตั้งค่าสำหรับควบคุมการรบกวนกันของข้อมูลระหว่างธุรกรรม โดยผ่อนคลายลงตามลำดับ Serializable → Repeatable Read → Read Committed → Read Uncommitted
- Serializable: ทำงานเสมือนว่าทุกธุรกรรมถูกรันแบบลำดับทีละรายการ
- Repeatable Read: เมื่อรันคิวรีเดิมซ้ำจะได้ผลลัพธ์เหมือนเดิม แต่ยังอาจเกิด phantom read ได้
- Read Committed: สามารถอ่านการเปลี่ยนแปลงจากธุรกรรมอื่นที่คอมมิตแล้วได้
- Read Uncommitted: อนุญาต dirty read เป็นระดับการป้องกันต่ำที่สุด แต่ให้ประสิทธิภาพสูง
การเขียนพร้อมกัน (Concurrent Writes)
- วิธีจัดการเมื่อธุรกรรมสองรายการแก้ไขแถวเดียวกันพร้อมกันนั้นแตกต่างกันไปตามแต่ละฐานข้อมูล
MySQL: การล็อกระดับแถว (Row-level Locking)
- Shared lock (S lock) อนุญาตให้หลายธุรกรรมอ่านพร้อมกันได้
- Exclusive lock (X lock) อนุญาตให้มีเพียงธุรกรรมเดียวที่แก้ไขแถวได้
- ในโหมด
SERIALIZABLE การอัปเดตทุกครั้งต้องได้ X lock และหากเกิดการชนกันอาจเกิด deadlock ได้
- MySQL ตรวจจับ deadlock และยุติหนึ่งในธุรกรรม
Postgres: Serializable Snapshot Isolation
- Postgres ใช้ predicate lock เพื่อติดตามการเข้าถึงในระดับชุดของแถว
- ตัวอย่าง: การล็อกสำหรับเงื่อนไข
WHERE id BETWEEN 10 AND 20
- ไม่ได้บล็อกการเข้าถึงจริงทันที แต่จะตรวจจับความขัดแย้งและยุติธุรกรรมเมื่อมีการละเมิด
- ใช้ การแก้ไขความขัดแย้งแบบมองโลกในแง่ดี (optimistic conflict resolution) เพื่อหลีกเลี่ยง deadlock
- เช่นเดียวกับ MySQL เมื่อเกิดความขัดแย้ง ธุรกรรมหนึ่งจะถูกยุติ และแอปพลิเคชันต้องมีตรรกะสำหรับลองใหม่
บทสรุป
- ธุรกรรมเป็นองค์ประกอบหลักของฐานข้อมูลที่รับประกัน ความเป็นอะตอม ความสอดคล้อง การแยกตัว และความคงทน (ACID)
- Postgres และ MySQL บรรลุเป้าหมายเดียวกันผ่านโครงสร้างภายในที่แตกต่างกัน
- หากเข้าใจระดับการแยกทั้ง 4 แบบและหลักการทำงานของธุรกรรม ก็จะดูแลฐานข้อมูลได้อย่างมั่นคงยิ่งขึ้น
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
รู้สึกว่าบทความนี้ยังอธิบายได้ไม่ค่อยครบ
แทนที่จะอธิบายระดับการแยกกันทำงานผ่าน phenomena ตามที่นิยามไว้ในมาตรฐาน SQL ผมคิดว่าการเริ่มจากแนวคิด serializability จะเข้าใจได้ตรงไปตรงมามากกว่า
serializability มองได้ว่าเป็นการขยายแนวคิดของ thread safety และเมื่อขาดสิ่งนี้ไป ก็จะเกิดบั๊กที่ผลลัพธ์เปลี่ยนไปตามลำดับการรัน
ระดับ isolation ต่าง ๆ ของฐานข้อมูลก็เป็นเพียงการผ่อนคลายการรับประกันนี้ลงเท่านั้น และผู้ใช้ต้องหาวิธีรับประกันในแบบอื่นเอง
phenomena เป็นเพียงเครื่องมือสำหรับทำภาพให้เห็นสถานการณ์ที่ไม่เป็น serializable ไม่ได้เชื่อมกับ serializability โดยตรง
ยกตัวอย่างเช่น Kubernetes cluster ก็สามารถทำงานแบบ serializable ได้ หากใช้ controller ที่ออกแบบมาดี
การจะพูดถึง transaction, isolation level และ MVCC พร้อมทั้งเปรียบเทียบระหว่างหลาย DB ในคราวเดียวเป็นงานที่ใหญ่มาก
ผมพยายามหาสมดุลระหว่างความลึกทางเทคนิค ความเข้าถึงง่าย และความยาวของบทความ
เป็นความเห็นว่าอยากให้มี notation และ citation มากกว่านี้
แต่ถ้าใช้โดยไม่จำเป็น ต้นทุนในการประสานงาน ระหว่าง transaction จะสูงขึ้น ทำให้ concurrency และ throughput ลดลง
เราอาจมอง transaction เหมือน snapshot ของ copy-on-write filesystem (btrfs, zfs) ได้ แต่ผมคิดว่าเปรียบกับ Git branch จะเข้าใจง่ายกว่า
BEGIN คือการสร้าง branch, UPDATE คือ commit, ROLLBACK คือการลบ branch, และ COMMIT ก็เหมือน
git mergeถ้ามี conflict DB จะพยายาม merge ในระดับ row และถ้าล้มเหลวก็จะ rollback หรือ force merge ตามการตั้งค่า
READ UNCOMMITTED เน้น merge ให้เร็ว ส่วน SERIALIZABLE ให้ความสำคัญกับความถูกต้อง
อุปมาแบบนี้อาจช่วยให้บางคนเข้าใจแนวคิดของ transaction แบบ “อ๋อ!” ได้
สิ่งที่ทำให้หลายคนแปลกใจคือ Postgres กับ MySQL ไม่ได้ใช้ serializable mode เป็นค่าเริ่มต้น แต่เป็น read-committed
ความต่างด้านประสิทธิภาพไม่ได้แค่ “เล็กน้อย” แต่ในทางปฏิบัติใหญ่กว่านั้นมาก
ถ้าใช้ read-committed ก็ต้องใส่ใจกับการจัดการ lock และ UNIQUE constraint ก็จำเป็นต่อการกัน race condition
ถึงอย่างนั้นผมก็ยังชอบวิธีนี้มากกว่าการยอมรับภาระด้านประสิทธิภาพและปัญหา retry ของ serializable mode
อ้างอิง: เอกสาร PostgreSQL
ดู เอกสาร MySQL, เอกสาร MariaDB
ทุกวันนี้แทบไม่มีใครใช้ MyISAM แล้ว
แอปพลิเคชันต้องตรวจจับให้ได้และมีกลยุทธ์ retry
serializable mode ดูดีมากในตำรา แต่ของจริงแทบไม่มีใครใช้
ทุกวันนี้เครื่องมือฐานข้อมูลจำนวนมากให้ความสำคัญกับ การแชร์อัปเดตแบบเรียลไทม์ มากกว่า ACID
ตัวอย่างเช่น Airtable จะแสดงผลการแก้ไขฟิลด์บนหน้าจอของเพื่อนร่วมงานทันที แต่เพราะไม่มี transaction จึงมี ความเสี่ยงเรื่องข้อมูลไม่สอดคล้องกัน
ดูเพิ่มเติมได้ที่ บทความบล็อก VisualDB
การอ่านบล็อก PlanetScale เป็นเรื่องที่เพลิดเพลินมากจริง ๆ
สงสัยว่าใช้เครื่องมืออะไรทำภาพประกอบ
ภาพประกอบทำด้วย js + gsap (https://gsap.com)
ถ้าสนใจหัวข้อนี้ ขอแนะนำ 『Designing Data-Intensive Applications』 อย่างมาก
มันไม่ได้ครอบคลุมแค่ isolation level หลายแบบ แต่ยังพูดถึง ความคลุมเครือของนิยาม ACID ด้วย
ได้ยินมาว่าฉบับพิมพ์ครั้งที่ 2 กำลังจะออก
transaction ใน ระบบ MVCC อย่าง Postgres คล้ายกับ snapshot ของ copy-on-write filesystem
ตอน BEGIN จะสร้าง snapshot ของข้อมูลขึ้นมา และ UPDATE จะสะท้อนอยู่แค่ในสำเนาส่วนตัว
ถ้า ROLLBACK สำเนานั้นก็จะถูกทิ้งไป และเมื่อ COMMIT snapshot ใหม่จะกลายเป็นเวอร์ชันทางการ
อุปมานี้อาจเป็นจุดที่ช่วยให้บางคนเข้าใจแนวคิด transaction ได้ชัดเจนขึ้น
P.S. จะเปรียบเป็น Git branch ก็ได้เหมือนกัน
ในกรณีอย่าง SELECT แล้วค่อย UPDATE อาจมี thread หนึ่งถูก block ได้
วันนี้ผมว่าจะลองทดสอบใน MySQL ว่าจะเปลี่ยนให้เป็น query เดียวได้ไหม
สมัยก่อนในการสัมภาษณ์ backend มักจะถามเรื่อง transaction บ่อยมาก
ทุกคนเคยใช้มัน แต่ระดับความเข้าใจต่างกันไปตามประสบการณ์
ต่อให้จำ isolation level ได้ไม่ครบทั้งหมด แค่รู้ว่ามันทำงานต่างกันก็พอให้เห็น ความอยากรู้อยากเห็นและความเข้าใจระบบ ได้แล้ว
คำอธิบายของ “phantom read” อาจทำให้เข้าใจผิดได้
ใน repeatable read ค่าของ row เดิมจะไม่เปลี่ยน แต่ row ใหม่ สามารถถูกเพิ่มเข้ามาได้
จะไม่มีกรณีที่ row เดิมถูกแก้ไขหรือลบ ดังนั้นควรอธิบายจุดนี้ให้ชัดเจน
ประโยคที่ว่า “ไม่เกี่ยวกับ xmin/xmax” ดูเหมือนจะอธิบายไม่ครบ
และการทำภาพตอน commit ที่ชี้ไปยัง table header ก็ดูแปลก ๆ
จริง ๆ แล้ว xmax/xmin คือกลไกหลักในการตัดสินสถานะ commit ไม่ใช่หรือ?
ถ้ารวม subtransaction เข้าไปด้วยก็ยิ่งซับซ้อนขึ้น
ถึงอย่างนั้นโดยรวมแล้วทั้งภาพและคำอธิบายก็อ่านสนุกมาก
มันเป็นหัวใจสำคัญในการเข้าใจ isolation level และให้ความรู้สึกเหมือนมีส่วนหนึ่งของบทความหายไป