มาทำความเข้าใจการเขียนโปรแกรมคุม Transaction กัน

Tae Keerati Jearjindarat
4 min readAug 29, 2021
https://unsplash.com/photos/Q59HmzK38eQ

สวัสดีครับ เรื่องนี้ก็เป็นเรื่องที่งงตั้งแต่สมัยเรียน(อีกแล้ว) เพราะตอนเรียนวิชา Database Management ประมาณ ปี 2 ได้ฟังอาจารย์พูดเรื่อง Database Transaction บ่อยมาก แต่ก็เรียนแค่ในมุมของการเขียน SQL ว่าต้องเขียนยังไง

ซึ่งมันเป็นช่วงเวลาที่เราได้ทำ App หลายตัวพอดี แต่ก็ไม่เข้าใจว่าการเขียน Transaction ด้วย SQL เกี่ยวอะไรกับตอนทำ App เพราะเราไม่เคยแตะเรื่อง Transaction ของ Framework ที่ใช้เลย เช่น Node Sequelize หรือ Laravel

เนื่องด้วยตัวเราก็ไม่เคยเขียนเกี่ยวกับ Transaction เลย พอมาเจอ Spring เราก็เจอ Magic ที่เป็น Annotation @Transactional ก็เลยงงไปอีกว่า เอ้ามันเอาไว้ทำอะไร

บล็อกนี้ก็จะเขียนเกี่ยวกับเรื่อง Transaction ในมุมที่เกี่ยวกับการใช้งานจริง ๆ ว่าเวลาเราเขียนโปรแกรมที่เกี่ยวกับ Transaction มันเป็นยังไง ซึ่งบอกก่อนว่าในบล็อกนี้จะพูดถึง Local Transaction เท่านั้นนะครับ

Transaction คืออะไร?

คำถามแรกสำหรับคนที่ยังไม่รู้ว่ามันคืออะไรนะครับ

keyword ก็คือ unit of work ของสิ่งที่เราทำกำลังทำบน DBMS ครับ ต้องเป็น Action หนึ่งของระบบ อาจจะเป็นจาก User หรือ System ก็ได้ เช่น มุม User ก็คือลูกค้าซื้อตั๋วเครื่องบิน หรือ มุม System ก็คืออาจจะมีระบบสุ่มที่นั่งให้ลูกค้าทันทีเมื่อมีการซื้อตั๋ว ทั้งหมดจะถูกบันทึกลง Database ในที่สุด

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

Transaction Key Properties

สิ่งสำคัญอีกอย่างก็คือ ACID ที่เป็น Properties สำคัญของ Transaction ประกอบไปด้วย 4 ตัวดังนี้

  • Atomicity คือเราบอกว่าทุก Transaction ที่เป็นหน่วยย่อยเล็ก ๆ เมื่อเกิดแล้วก็ต้องทำให้สำเร็จจริง ๆ หรือถ้ามันพังก็ต้องพังทั้งหมด เหมือนกับตัวอย่างการจองตั๋ว คือถ้าจองได้คือได้ตั๋วพร้อมที่นั่ง แต่ถ้าที่นั่งเต็มต้องไม่ส่งตั๋วให้ลูกค้า
  • Consistency คือเราจะต้องมั่นใจได้ว่า Transaction ที่เก็บลง Database เป็นสิ่งที่ถูกต้องจริง ๆ เป็น Valid State ที่เกิดจากการทำ Atomicity และต้องเป็นสิ่งที่ถูกต้องตามกฏของ constaints ต่าง ๆ ที่เรากำหนด
  • Isolation คือแต่ละ Transaction ต้องไม่กระทบกัน โดยจะเกิดในกรณีมีหลาย Transaction ทำบนข้อมูลเดียวกัน จะต้องถูกจัดการเรื่อง Concurrency ด้วย เช่น ถ้ามีคนจองตั๋วเครื่องบิน พร้อมกัน 2 คน แต่เหลือโควต้าแค่ 1 ที่นั่ง จะต้องมีคนจองสำเร็จแค่คนเดียว
  • Durability คือการการันตีว่า Transaction ที่ถูก commit สำเร็จเสร็จสิ้นแล้ว จะต้องยังอยู่เหมือนเดิม แม้ว่าจะไฟจะดับ หรือ Thanos ถล่มโลก

ผมเขียนเรื่อง Concurrency Control ไว้ ถ้าอยากอ่านเพิ่มเติม เชิญได้ที่
https://imgrbs.medium.com/concurrency-control-101-6b9b8183936a

วิธีเขียน SQL Transaction

https://unsplash.com/photos/goFBjlQiZFU

จาก Properties ที่ได้กล่าวไปว่า ACID ทำให้ตัว DBMS ในหลาย ๆ เจ้าได้ทำ Feature Transaction เอาไว้ให้เราใช้งาน โดยการันตี ACID ไว้ให้แล้ว (แต่ก็ขึ้นอยู่กับการตั้งค่า Database Isolation Level ของเราด้วย ต้องตรวจสอบดูดี ๆ) มีขั้นตอนง่าย ๆ ดังนี้

  1. ประกาศเริ่ม Transaction
  2. เขียน Executable Query เพื่ออัพเดทหรือปรับเปลี่ยนข้อมูล
  3. ถ้าไม่มี Error ให้สั่ง Commit
  4. ถ้าเจอ Error ให้สั่ง Rollback ทั้งหมด

Example Usecase

สมมติว่าถ้ามีลูกค้าพยายามซื้อตั๋วเครื่องบิน เราก็ต้องมีตารางที่เก็บข้อมูลเรื่องตั๋ว และ เที่ยวการบิน จากนั้นเราก็ต้องพยายามจะอัพเดทว่าตอนนี้จำนวนคนที่นั่งมีเท่าไร ถ้าไม่เกินโควต้า จึงจะสามารถออกตั๋วให้ลูกค้าได้ โดยจะสมมติว่ามี Quota แค่ 30 ส่งผลให้มีความเป็นไปได้ดังนี้

  1. ถ้าจำนวน number_of_seats น้อยกว่าหรือเท่ากับ 30 = COMMIT ได้ปกติ
  2. ถ้าจำนวน number_of_seats เกิน 30 = เกิด Error และ ROLLBACK

จากตัวอย่าง SQL คือ เรามีตารางชื่อ flight และผูก Constraint ไว้ดังนี้

  • ตรวจสอบ max_quota คือจำนวนที่นั่งสูงสุด มีค่าไม่เกิน 30
  • ตรวจสอบ number_of_seats คือจำนวนที่นั่งปัจจุบัน ต้องไม่เกิน max_quota

จากนั้นเราค่อยพยายาม Select ข้อมูลเที่ยวบินและคนซื้อออกมาก่อนเพื่อเตรียม Data สำหรับอัพเดทเที่ยวบินอีกรอบ และ สร้างตั๋ว เราจะเห็นว่าถ้าจำนวนที่นั่งเกินโควต้า จะทำให้ไม่ผ่าน Constraint แต่ถ้าไม่เกิน จะสามารถ Commit เพื่อสร้างตั๋วได้ปกติ

แต่พอเราทำ Application จริงๆ เราไม่ควรใช้ Constraint ผูกเงื่อนไขของ Business ไว้ใน Database แบบนี้เพราะจะทำให้ Maintain ยาก

อยากให้นึกภาพเวลาเราต้องเขียนโปรแกรมเพื่อบันทึกข้อมูล เราต้องดึงข้อมูลบางอย่างมาเช็กและคำนวณ คำถามคือทำไมเราต้องมานั่งดูว่า Column ที่เราทำงานอยู่ มี Contstraint อะไรบ้าง แทนที่เราจะไล่จาก Business Code ในระดับ Service ได้เลยซึ่งง่ายกว่า ทั้งนี้ยังมีประโยชน์อีกเพราะทำให้ Source Code เป็น Souce of Truth ส่งผลให้เราแค่กำหนด Data Type และ Key Constraint ส่วน Business Logic ต่าง ๆ ให้เราไปไล่โค้ดเอา

Transaction on Application Level

https://unsplash.com/photos/v9FQR4tbIq8

จากตัวอย่างที่เห็นแบบ SQL ไป ทำให้เราพบว่าสิ่งที่ทำจริง ๆ ก็คือเราต้องเขียน Business Logic ลงในโค้ดตามภาษาที่เราใช้ ไม่ต้องผูก Contraint ยุบยับ แต่ใช้การเขียน Logic ด้วยการเขียนโปรแกรมไป เราควรให้ค่ากับการ Software Design ให้ตรงตาม Requirement และสามารถ Maintain ได้ง่าย ๆ มากกว่า

ส่วนภาพสุดท้ายของโค้ดที่เกี่ยวกับ Transaction ก็จะคล้าย ๆ การเขียนด้วย SQL นั่นแหละครับ แต่ต่างกันที่เวลาเราเขียนโปรแกรมจะค่อนข้างยืดหยุ่นกว่า และ Maintain ง่าย และ ถ้าใช้ Framework ที่จัดการเรื่อง Object Relational Mapping (ORM) ให้แล้วก็จะสบายไปอีก ซึ่ง Framework ต่างๆ ก็มักจะทำเรื่องการแปลง Business Code เป็น Query ให้แล้ว ทำให้เราใช้แค่ Interface ที่เค้ามีให้ และ โฟกัสในส่วน Business Logic ให้มากๆ ได้เลย ซึ่งเราอาจจะใช้ตัวไหนก็ได้ตามถนัด เช่น JPA, Hibernate, Prisma, Nestjs หรือ Laravel ฯลฯ

ซึ่งจากที่ดู Framework ในหลาย ๆ ภาษาเราสามารถจำแนกการจัดการ Transaction ได้ 2 ประเภทด้านล่าง โดยผมขอยกตัวอย่างของ Spring Framework นะครับ เพราะของครบ และต้องบอกว่าไม่ใช่ทุก Framework ที่จะมีเหมือนกัน แต่น่าจะมีคล้าย ๆ กัน

1. Programmatic Transaction Management

คือการที่เราเขียนโค้ดเพื่อควบคุม Transaction ด้วยตัวเองทั้งหมด โดยเราควบคุมผ่าน Interface ของภาษาหรือ Framework นั้นๆ ได้โดยตรง ข้อดีคือเราสามารถคุม Transactionได้อย่างยืดหยุ่น จะทำอะไรก็ได้ เช่น เกิด error แต่ยังอยากให้ commit

ข้อเสียคือ Maintain ยาก เพราะในโค้ดจะมีส่วนที่เปิด-ปิด Transactionหลายจุด อาจทำให้ซ้ำและสับสนได้ง่าย

ผมเข้าใจว่าอันนี้เป็นประเภทที่น่าจะทำได้ในทุก ๆ ภาษา เนื่องจากเบื้องต้นผมลองไล่หา Docs ของ Golang, Laravel หรือ Nodejs ก็มีคนเขียน Example วิธีนี้ไว้อยู่ ซึ่งขึ้นอยู่ว่าใช้ Framework ไหนด้วย แต่วิธีจะคล้าย ๆ กันหมด ซึ่งภาพที่ได้ก็จะประมาณตัวอย่างนี้ครับ

Java Example

จากตัวอย่างโค้ดผมใช้ Java และ Spring Framework โดยเค้ามี Transaction Template ให้ใช้โดยจะบังคับใช้คู่กับ Platform Transaction Manager เป็นส่วนที่คอยควบคุมคำสั่งที่เกี่ยวกับ transaction ตาม Framework ที่เราใช้เช่น เช่น Hibernate, JPA หรือ JDBC

โดยในโค้ดเราจะสร้าง TransactionTemplate ขึ้นมาและสามารถสั่งให้ Execute Transaction โดยใช้โค้ด Java ปกติได้เลย ในตัวอย่างโค้ดจะเป็นเรื่องการซื้อตั๋วเครื่องบิน ที่คล้าย ๆ กับใน SQL แต่ Logic จะมาอยู่ในโค้ดของเรา ทำให้สามารถเขียน Unit Test ได้ และ Maintain ง่ายกว่าด้วยนั่นเอง

2. Declarative Transaction Management

คือการที่เราแยกโค้ดที่ควบคุม Transaction แบบ Programmatic ออกจาก Business Code ทิ้งไว้ และสามารถเรียกใช้ได้ง่าย ๆ ข้อเสียก็คือไม่ค่อยยืดหยุ่น

ประเภทนี้ที่ขึ้นอยู่กับ Framework ที่เราใช้ว่ามี Interface มาให้หรือไม่ อย่างเช่นถ้าใช้ Spring ก็จะมี @Transactional มาให้ใช้ได้ง่าย ๆ แต่ถ้า Framework ที่ใช้ไม่มีอะไรแบบนี้ก็สามารถเขียนขึ้นมาเองได้ โดยอิง Concept เหมือน Spring

เนื่องจาก Spring ใช้ Concept ของ Aspect-Oriented Programming (AOP) ในการ Implement และใช้วีธีนี้กับส่วนของ Transaction Management ด้วย โดยจะใช้ Design Pattern ที่ชื่อว่า Proxy ในการพัฒนาขึ้นมา

แปลว่าเมื่อเราใส่ @Transactional บนหัว Method ก็จะใช้งานได้เลย โดยจะทำ Proxy และไปเรียกโค้ดเบื้องหลังที่ประกอบด้วย 2 ส่วนคือ

  • Transaction Interceptor เป็นตัวที่คอยดักและจัดการ lifecycle ของ method นั้นๆ ในที่นี้หมายถึงว่า จะควบคุมช่วงการทำงานก่อนที่ method จะเริ่ม และบังคับให้ใช้คำสั่ง begin transaction เสมอ และ ควบคุมอีกรอบหลังจาก method จบการทำงาน จะบังให้ทำการ commit หรือ rollback นั่นเอง
  • Platform Transaction Manager อันเดียวกับที่ใช้ใน Programmatic คือเป็นส่วนที่คอยควบคุมคำสั่งที่เกี่ยวกับ transaction

Example

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

สุดท้าย

สำหรับการเขียนโปรแกรมคุม Transaction ในแต่ละภาษาไม่ต่างกันมาก แทบจะไม่ต่างกับ SQL เลย อยู่ที่จะ Implement ยังไงและให้ความสำคัญกับอะไรมากกว่า

แต่สุดท้ายยังไงเราต้องคำนึงถึงข้อสำคัญของ Transaction และใช้ Framework อย่างเข้าใจอยู่ดี จริงๆ ยังมีเรื่องการกำหนด boundaries ของ Transaction ด้วย แต่ไว้โอกาสหน้าครับ

References

--

--

Tae Keerati Jearjindarat

Hi, I'm Tae, Associate Engineering Manager at LINEMAN Wongnai. Thanks for following <3