Concurrency Part 1 - ปัญหาพื้นฐานเมื่อใช้งาน Database พร้อมกัน

Concurrency Part 1 : ปัญหาพื้นฐานเมื่อใช้งาน Database พร้อมกัน

ผมเขียนเกี่ยวกับ Concurrency ไว้หลายตอน คุณสามารถกด Link ด้านล่างเพื่ออ่านที่เกี่ยวกับ Concurrency ตอนต่างๆได้เลย

ปัญหาการใช้ Database พร้อมกันจากหลาย Process (หรือหลาย Thread หลาย Request เอาเป็นว่าเข้ามาใช้งานพร้อมๆกัน) เป็นปัญหาพื้นฐานที่ Programmer ทุกคนน่าจะเคยเจอ (จริงๆมันไม่ใช่เฉพาะ Database แต่มันเป็นปัญหาในการใช้งาน Resource พร้อมกัน) แต่ต่อให้เป็นปัญหาพื้นฐานแต่การพูดถึงเรื่องนี้นี่น้อยมาก เด็กจบใหม่หลายคนไม่ค่อยรู้เรื่องนี้ (เขาจบใหม่อะนะ) แล้วต้องมาทนทุกข์ทรมาณเวลาเจอปัญหาเหล่านี้ (เขียนเกือบเสร็จแล้วค่อยมารู้) บทความนี้เลยอยากพูดถึงเรื่องนี้เพื่อให้เกิดการตระหนักรู้เกี่ยวกับปัญหานี้ เวลาเขียนโปรแกรมจะได้ Design โปรแกรมให้ป้องกันปัญหานี้

ปัญหา Lost update

สมมุติกรณีเป็น

Process A ต้องการหักเงินจากบัญชีของคุณเป็นจำนวนเงิน 100 บาท

Process B ต้องการหักเงินจากบัญชีของคุณเป็นจำนวนเงิน 200 บาท

เงินในบัญชีคือ 500

ปัญหาแรกที่เราจะพูดถึงคือ Lost update Problem ตัวอย่างคือ Process A อ่านค่าเงินออกไป 500 คำนวนแล้วจะต้องบันทึกข้อมูลไปเป็นจำนวน 400 ส่วน Process B ก็อ่านเงินออกไป 500 ไปคำนวนแล้วต้องบันทึกเป็นเงิน 300 ลง Database จะเห็นว่า Process B ทำการบันทึกค่าจากนั้น Process A ทำการบันทึกค่าจะเห็นว่าการบันทึกค่าของ Process B หายไปเหลือแต่การบันทึกค่าของ Process A (ซึ่งก็ไม่ถูกอยู่ดี)

ปัญหา Dirty Read

สมมุติกรณีเป็น

Process A เป็นงาน Batch ทำการตัดเงินจากบัญชีหลายรายการโดยตัด 2 รายการ รายการแรก 100 รายการที่สอง 100
Process B ดึงข้อมูลเงินในบัญชีออกไปเพื่อไปแสดงผลให้ลูกค้าดู

เงินในบัญชีคือ 500

ปัญหาต่อมาคือ Dirty Read ว่าง่ายๆคือเรากำลังอ่านข้อมูลที่ยังไม่สมบูรณ์ ถ้าในตัวอย่างคือ Process A กำลังทำงาน Batch ตัดเงินในบัญชีอยู่ ระหว่างที่กำลังตัดรายการแรกเสร็จ ตัว Process B เข้ามาทำการอ่านข้อมูลไปแสดงผลว่าบัญชีมีเงินอยู่เท่าไหร่ เพื่อเอาไปใช้ในงานสำคัญบางประการ ซึ่งเข้ามาอ่านตอนที่ Process A ยังทำงานไม่เสร็จทำให้อ่านข้อมูลที่ยังไม่เสร็จไป

ปัญหา Non Repeatable Read

สมมุติกรณีเป็น

Process A อ่านข้อมูลทุกบัญชีในธนาคารเพื่อ Sum ยอดรวมออก Report

Process B ต้องการหักเงินจากบัญชีของคุณเป็นจำนวนเงิน 200 บาท

เงินในบัญชีคือ 500

ปัญหาต่อมาจะพูดกันคือ Non Repeatable Read ว่าง่ายๆคืออ่านถ้าอ่านข้อมูลสองรอบแล้วได้ค่าไม่เท่ากัน (คาดหวังว่าจะอ่านทุกครั้งแล้วได้ค่าเหมือนเดิม แม้จะไม่เข้าไปอ่านก็ตาม) ดังตัวอย่างในภาพ Process A ดึงข้อมูลทุกบัญชีเพื่อไปออก Report ในระหว่างนั้นมี Process B เข้ามาทำการเขียนข้อมูลลงใน Account X ทำให้ค่าเงินมันเปลี่ยนไปส่งผลให้รายงานที่ออกมีข้อมูลไม่ถูกต้อง (จริงๆปัญหา Lost update อันแรกก็เกิดจาก Non Repeatable Read เหมือนกัน)

วิธีแก้นั้นมีแล้ว ง่ายมากๆ ใช้ Lock ไงล่ะ

วิธีการแก้ปัญหาพวกนี้นั้นมีอยู่แล้วครับ ไม่ยากด้วย (แต่ดีรึเปล่าอีกเรื่อง) วิธีแก้ปัญหาเหล่านั้นก็คือ Lock ข้อมูลที่จะใช้เลยจ้า ตัวอย่างเช่น ถ้าเราต้องการจะ Update ข้อมูลบัญชี X เราก็ Lock บัญชี X ไปเลย เราจะเป็นคนเดียวที่ใช้บัญชี X ได้ คนอื่นที่จะมาใช้งานจะต้องรอจนกว่าเราใช้เสร็จถึงจะเข้าไปงานบัญชี X ได้ดังตัวอย่าง Lost update ถ้าเราใช้ Lock ช่วยก็การทำงานจะเป็นดังภาพด้านล่าง ซึ่งผลลัพธ์ที่ออกมานั้นถูกต้อง

แต่เดี๋ยวก่อนนะครับอย่าพึ่งดีใจกันไปถ้าทุกอย่างมันง่ายอย่างงี้ก็คงดี คือเมื่อเราเริ่มใช้ Lock ปัญหาต่อมาก็คือ เราไม่สามารถใช้งานข้อมูลนั้นได้เลย ถ้าสมมุติมี Process ที่ต้องการเข้ามาแค่อ่านข้อมูลเฉยๆ ไม่ได้จะแก้ แต่เมื่อใช้งาน Lock แล้วกลายเป็นว่าต้องรอให้ Process ก่อนหน้าทำงานเสร็จก่อนดังภาพ ซึ่งการเกิดปัญหาความเร็วในการทำงานโดยรวมขึ้นมานั่นเอง

แล้วจะแก้ไงล่ะ ก็เพิ่มระดับ Lock ไงล่ะคุณผู้ชม โดยเราจะทำการสร้าง Lock ขึ้นมา 2 แบบคือ

  1. Read lock (บางที่เรียก Share lock)

    ตัว Read lock จะทำการ Lock แต่มีเงื่อนไขว่าถ้าคนที่ขอใช้ข้อมูลที่ Lock อยู่เนี่ยขอแบบ Read Lock เหมือนกันก็จะยอมให้ใช้งาน (มันเลยมีบางที่เรียกว่า Share lock เพราะมัน Share กันได้) แต่ถ้าขอแบบ Write Lock จะไม่ยอมให้ใช้ข้อมูล ส่วนใหญ่การขอ Read lock จะขอเมื่อต้องการอ่านข้อมูลเฉยๆไม่อยากแก้ไข

  2. Write lock (บางที่เรียก Exclusive lock)

    ตัว Write lock จะทำการ Lock ข้อมูลแบบให้ใช้ได้แค่คนที่ขอเท่านั้น ใครมาขอใช้ Lock แบบไหนจะไม่ยอมให้เลย ส่วนใหญ่การขอ Write lock จะขอเมื่อต้องการแก้ไขข้อมูลนั้น

ดังนั้นปัญหาการ Export Report ด้านบนก็จะถูกแก้ด้วยการใช้ Read lock ในการดึงข้อมูลนั่นเอง

การจัดการด้วย Lock มันเหมือนง่ายแต่ยากนะ

ในตัวอย่างที่ผ่านๆมาการ Lock นั้นเข้าถึงข้อมูลตัวเดียว และ Process ที่เข้ามาใช้งานมีแค่ 2 Process แต่ถ้า Process ที่เข้ามาใช้งานมีหลายตัวและเข้าถึงข้อมูลหลายตัวเช่นกัน แล้วปัญหาแรกๆที่เราจะเจอเมื่อใช้ Lock คือ Dead lock ซึ่งก็คือสถานะไปต่อไปไม่ได้โดยปัญหาที่แสดงการเกิด Dead lock ได้ง่ายที่สุดคือ

Dining philosophers problem

โจทย์นี้ไม่มีอะไรมากเลยคือ นักปราชญ์มานั่งล้อมวงคิดงานสำคัญกัน แต่ถ้านั่งคิดอย่างเดียวก็กลัวจะหิวเลยวางมีของกินวางไว้ด้วย แต่กลัวนักปราชญ์จะกินอย่างเดียวก็เลยว่างส้อมไว้แค่ 5 คัน โดยจะกินอาหารได้ต้องใช้ส้อม 2 คันเท่านั้น และด้วยความที่กลัวจะเขินเวลาหยิบส้อมมาจะกินอาหาร นักปราชญ์ก็เลยตั้งกฏขึ้นมาว่าถ้าหยิบส้อมขึ้นมาแล้วต้องได้กินอาหารถึงจะวางส้อมได้

ปัญหาดูดีๆเหมือนไม่มีอะไร แต่ถ้านักปราชญ์ทุกคนพร้อมใจกันหยิบซ้อมคนละอันขึ้นมาอะไรจะเกิดขึ้น (ไม่ใช่แทงกันนะครับ) ก็ง่ายๆครับทุกคนจะไม่สามารถกินอาหารได้เนื่องจากมีส้อมแค่อันเดียว และด้วยกฏที่ว่าต้องกินถึงจะวางส้อมได้ทำให้ทุกคนวางส้อมไม่ได้ สุดท้ายก็ติดอยู่ในสถานะ Dead lock นั่นเอง

ตัดจบก่อนละกัน

สำหรับตอนนี้เราได้พูดถึงปัญหาที่จะเกิดขึ้นเมื่อใช้งาน Database พร้อมกันไม่ว่าเป็น Lost update , Dirty read, Non repeatable read และเราได้แสดงวิธีการแก้ปัญหาโดยการใช้ Lock ในการ Lock ข้อมูลซึ่งต้อง Implement เอง แต่จริงๆแล้วตัว Database มี Feature ที่จัดการกับปัญหาเหล่านี้เอาไว้แล้ว (อ้าว แล้วจะมีตอนนี้เพื่ออะไรวะ) ดังนั้นในตอนหน้าเรามาดูวิธีการใช้ Feature ของ Database ในการแก้ปัญหากัน

Reference

https://onlinelibrary.wiley.com/doi/abs/10.1002/(SICI)1097-024X(199801)28:1%3C77::AID-SPE148%3E3.0.CO;2-R

เพลงประกอบการเขียน Blog

เพลง Nice to Miss You ของน้องๆวง Wishdom