Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Somprasong Damyos
Somprasong Damyos

Posted on

ทำไมต้องใช้ Database Transaction?

Originally published athttps://somprasongd.work/blog/db/dbtx

ในการพัฒนาระบบที่เกี่ยวข้องกับข้อมูลสำคัญ เช่น การเงิน หนึ่งในสิ่งที่ขาดไม่ได้คือการใช้Database Transaction เพื่อให้มั่นใจว่าข้อมูลจะถูกจัดการอย่างถูกต้องและปลอดภัย โดยเฉพาะในสถานการณ์ที่มีหลายคำสั่ง SQL ทำงานร่วมกัน เช่น "การโอนเงิน" ที่ต้องอัปเดตยอดเงินสองบัญชีพร้อมกัน

ในบทความนี้ เราจะพาคุณเข้าใจว่าเหตุใดการใช้Transaction จึงสำคัญ ผ่านการเรียนรู้แนวคิดACID,Lock, และDeadlock ด้วยตัวอย่างจากการโอนเงินระหว่างบัญชีด้วย PostgreSQL


โครงสร้างฐานข้อมูล:accounts และtransactions

CREATETABLEpublic.accounts(idserial4NOTNULL,"name"textNOTNULL,balancenumericNOTNULL,CONSTRAINTaccounts_pkeyPRIMARYKEY(id));CREATETABLEpublic.transactions(idserial4NOTNULL,from_account_idint4NULL,to_account_idint4NULL,amountnumericNOTNULL,created_attimestampDEFAULTnow()NULL,CONSTRAINTtransactions_pkeyPRIMARYKEY(id),CONSTRAINTtransactions_from_account_id_fkeyFOREIGNKEY(from_account_id)REFERENCESpublic.accounts(id),CONSTRAINTtransactions_to_account_id_fkeyFOREIGNKEY(to_account_id)REFERENCESpublic.accounts(id));
Enter fullscreen modeExit fullscreen mode

ปัญหา: การโอนเงินโดยไม่ใช้ Transaction

สมมุติว่าเราต้องโอนเงิน 500 บาทจากบัญชี A ไปยังบัญชี B โดยใช้คำสั่ง SQL ต่อไปน

UPDATEaccountsSETbalance=balance-500WHEREid=1;UPDATEaccountsSETbalance=balance+500WHEREid=2;INSERTINTOtransactions(from_account_id,to_account_id,amount)VALUES(1,2,500);
Enter fullscreen modeExit fullscreen mode

ถ้าเกิดเหตุขัดข้องหลังอัปเดตบัญชี A แล้ว (เช่น network error หรือ crash) แต่ยังไม่ทันเพิ่มเงินให้บัญชี B — เงิน 500 บาทจะ “หายไปจากระบบ” ซึ่งเป็นปัญหาหนักมากในระบบการเงิน


วิธีแก้: ใช้ Database Transaction

BEGIN;UPDATEaccountsSETbalance=balance-500WHEREid=1;UPDATEaccountsSETbalance=balance+500WHEREid=2;INSERTINTOtransactions(from_account_id,to_account_id,amount)VALUES(1,2,500);COMMIT;
Enter fullscreen modeExit fullscreen mode

หากคำสั่งใดล้มเหลว PostgreSQL จะไม่COMMIT และเราสามารถROLLBACK เพื่อยกเลิกทุกคำสั่งที่ทำไปแล้ว ทำให้ข้อมูลไม่เสียหาย


เข้าใจ ACID: หัวใจของ Transaction

ACID คือคุณสมบัติหลักของ Database Transaction ที่ทำให้ข้อมูลเชื่อถือได้ ซึ่งย่อมาจาก

  • Atomicity – ทุกคำสั่งใน transaction ต้องสำเร็จทั้งหมด หรือไม่สำเร็จเลย
  • Consistency – หลัง transaction สำเร็จ ฐานข้อมูลยังคงถูกต้องตามกฎ เช่น balance ≥ 0
  • Isolation – แต่ละ transaction ไม่รบกวนกันระหว่างทำงานพร้อมกัน
  • Durability – เมื่อCOMMIT แล้ว ข้อมูลจะถูกบันทึกถาวร แม้ระบบดับ

ตัวอย่าง: หากมีคนโอนเงินจากบัญชีเดียวกันสองคนพร้อมกัน PostgreSQL ต้องมั่นใจว่าไม่มีการขัดแย้ง หรือข้อมูลผิดเพี้ยน เช่นเงินติดลบ


การล็อกข้อมูล (Database Lock)

PostgreSQL จะล็อกrow-level อัตโนมัติเมื่อมีUPDATE:

UPDATEaccountsSETbalance=balance-500WHEREid=1;
Enter fullscreen modeExit fullscreen mode

แถวของบัญชีid = 1 จะถูกล็อกไว้จนกว่า transaction จะจบ เพื่อป้องกันไม่ให้ transaction อื่นเข้ามาแก้ไขบัญชีเดียวกันพร้อมกัน

สถานการณ์:select ก่อนupdate

แต่ในกรณีที่ใน application logic มีการดึงข้อมูล (select) ขึ้นมาก่อน แล้วทำการแก้ไขค่า เสร็จแล้วบันทึกกลับไปในฐานข้อมูล (update) แล้วเกิด transaction เข้ามาแก้ไขบัญชีเดียวกันพร้อมกัน เช่น

-- Transaction ABEGIN;SELECTid,"name",balanceFROMaccountsWHEREid=1;id|name|balance|--+----+-------+1|A|1000|UPDATEaccountsSETbalance=1000-500WHEREid=1;COMMIT;-- Transaction B (เกิดขึ้นพร้อมกัน)BEGIN;SELECTid,"name",balanceFROMaccountsWHEREid=1;id|name|balance|--+----+-------+1|A|1000|UPDATEaccountsSETbalance=1000-500WHEREid=1;COMMIT;
Enter fullscreen modeExit fullscreen mode

ในสถานการณ์นี้จะทำให้ค่าbalance ผิด แทนที่จะเหลือ 0 แต่กลับเหลือ 500 แทน

วิธีแก้: ใช้SELECT FOR UPDATE เพื่อล็อกก่อน

-- Transaction ABEGIN;SELECTid,"name",balanceFROMaccountsWHEREid=1FORUPDATE;id|name|balance|--+----+-------+1|A|1000|UPDATEaccountsSETbalance=1000-500WHEREid=1;COMMIT;-- Transaction B (เกิดขึ้นพร้อมกัน)BEGIN;SELECTid,"name",balanceFROMaccountsWHEREid=1FORUPDATE;-- จะหยุดรอจนกว่า Transaction A จะ COMMIT หรือ ROLLBACKid|name|balance|--+----+-------+1|A|500|UPDATEaccountsSETbalance=500-500WHEREid=1;COMMIT;
Enter fullscreen modeExit fullscreen mode

Deadlock: ล็อกขัดกันเอง

สถานการณ์ที่ 1:

  • Transaction A: โอน A ➜ B
  • Transaction B: โอน B ➜ A
-- Transaction ABEGIN;UPDATEaccountsSETbalance=balance-500WHEREid=1;UPDATEaccountsSETbalance=balance+500WHEREid=2;-- รอ เพราะ lock จาก Transaction B-- Transaction B (เกิดขึ้นพร้อมกัน)BEGIN;UPDATEaccountsSETbalance=balance-300WHEREid=2;UPDATEaccountsSETbalance=balance+300WHEREid=1;-- ERROR: deadlock detected
Enter fullscreen modeExit fullscreen mode

สถานการณ์ที่ 2:

ถ้าINSERT ไปยังtransactions ซึ่งมี Foreign Key อ้างถึงaccounts.id และหากมีอีก transaction กำลังUPDATE accounts อยู่ อาจเกิด Deadlock ได้

เช่น สมมุติให้เปลี่ยนลำดับให้บันทึกtransactions ก่อน แล้วมาดึงข้อมูลข้อมูลaccounts ก่อนupdate

-- Transaction ABEGIN;INSERTINTOtransactions(from_account_id,to_account_id,amount)VALUES(1,2,500);SELECTid,"name",balanceFROMaccountsWHEREid=1FORUPDATE;-- หยุดรอ เพราะ INSERT INTO transactions จาก Transaction B-- Transaction B (เกิดขึ้นพร้อมกัน)BEGIN;INSERTINTOtransactions(from_account_id,to_account_id,amount)VALUES(1,2,500);SELECTid,"name",balanceFROMaccountsWHEREid=1FORUPDATE;-- ERROR: deadlock detected
Enter fullscreen modeExit fullscreen mode

จากสถานการณ์ที่ยกตัวอย่างมา ทั้งสอง transaction จะล็อกบัญชีฝั่งของตนเองก่อน แล้วรอกันข้าม — ทำให้เกิดDeadlock PostgreSQL จะเลือก kill transaction ใด transaction หนึ่งเพื่อยกเลิก


วิธีป้องกัน Deadlock

  • ล็อก row ตามลำดับเสมอ
    ใช้ORDER BY id เพื่อให้ทุก transaction ล็อกบัญชีตามลำดับเดียวกัน เช่นid จากน้อยไปมาก

  • ล็อก row ก่อนอัปเดตด้วยFOR NO KEY UPDATE
    หากไม่ล็อก row ล่วงหน้า PostgreSQL จะพยายามล็อกเองในตอนที่INSERT ไปยังtransactions ซึ่งมี Foreign Key อ้างถึงaccounts.id
    หากอีก transaction กำลังUPDATE accounts อยู่ อาจเกิด Deadlock ได้
    FOR NO KEY UPDATE คือระดับ lock ที่

    • ป้องกันการ update, delete หรือ insert ที่อ้าง foreign key
    • เบากว่าFOR UPDATE แต่ปลอดภัยสำหรับกรณีแบบนี้
    • เหมาะสำหรับ row ที่เราจะอัปเดต (แต่ไม่เปลี่ยนค่า primary key)
  • ควรUPDATE accounts ก่อน แล้วค่อยINSERT transactions

    ช่วยลดความเสี่ยงในการเกิด Deadlock เพราะว่าหากคุณINSERT ก่อน และมี Foreign Key ไปที่accounts → PostgreSQL จะพยายามล็อก row เหล่านั้นก่อน ถ้าคำสั่งUPDATE accounts มาทีหลัง อาจเกิดDeadlock ได้ (โดยเฉพาะถ้าอีก transaction ล็อกกลับกัน)

ตัวอย่าง

-- Transaction ABEGIN;-- 1. ล็อกบัญชีล่วงหน้าเพื่อลดโอกาส deadlockSELECTidFROMaccountsWHEREidIN(1,2)ORDERBYidFORNOKEYUPDATE;-- 2. อัปเดตยอดเงินUPDATEaccountsSETbalance=balance-500WHEREid=1;UPDATEaccountsSETbalance=balance+500WHEREid=2;-- 3. บันทึกธุรกรรมINSERTINTOtransactions(from_account_id,to_account_id,amount)VALUES(1,2,500);COMMIT;-- Transaction B (เกิดขึ้นพร้อมกัน)BEGIN;SELECTidFROMaccountsWHEREidIN(1,2)ORDERBYidFORNOKEYUPDATE;-- รอจนกว่า Transaction A จะถูก COMMIT หรือ ROLLBACKUPDATEaccountsSETbalance=balance-300WHEREid=2;UPDATEaccountsSETbalance=balance+300WHEREid=1;INSERTINTOtransactions(from_account_id,to_account_id,amount)VALUES(2,1,300);COMMIT;
Enter fullscreen modeExit fullscreen mode

ตารางเปรียบเทียบระดับ Lock (ใน SELECT ... FOR ...)

Lock Modeใช้ในกรณีป้องกันไม่ให้ใครทำอะไรได้บ้าง
FOR UPDATEอัปเดต rowห้ามใคร update/delete แถวนี้
FOR NO KEY UPDATEอัปเดต row (ไม่แตะ key)ห้าม insert ที่อ้างถึง row นี้ (FK)
FOR SHAREอ่านอย่างเดียวห้ามใคร delete หรือ update แถวนี้
FOR KEY SHAREอ่านพร้อมอ้าง FKห้ามใครเปลี่ยน primary key หรือ delete

สรุป

การใช้Database Transaction เป็นสิ่งสำคัญอย่างยิ่งในระบบที่ต้องการความถูกต้องของข้อมูล โดยเฉพาะระบบการเงิน โดย PostgreSQL มีคุณสมบัติรองรับ ACID อย่างครบถ้วน พร้อมระบบ Lock และกลไกจัดการ Deadlock อัตโนมัติ

หากละเลยการใช้ Transaction อาจนำไปสู่ความเสียหายทางข้อมูล และความไม่ถูกต้องที่ยากจะแก้ไขภายหลัง

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

13+ years of hospital software development experience I always like to learn new technology to improve my skills. and share it with my teammates.
  • Location
    thailand
  • Joined

Trending onDEV CommunityHot

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp