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

Transaction basics

transaction เปลี่ยนหลายคำสั่งให้กลายเป็นหน่วยเดียวที่แบ่งแยกไม่ได้ คุณเปิดด้วย BEGIN รันคำสั่งที่ต้องการจัดกลุ่ม แล้วปิดด้วย COMMIT เพื่อเก็บงานไว้ หรือ ROLLBACK เพื่อทิ้งงานนั้น สิ่งที่อยู่ภายใน transaction จะไม่ปรากฏให้ connection อื่นเห็นจนกว่าคุณจะ commit

เราจะใช้ table accounts เล็ก ๆ ในทุกตัวอย่างของบทเรียนนี้

CREATE TABLE accounts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
owner text NOT NULL,
balance numeric NOT NULL CHECK (balance >= 0)
);
INSERT INTO accounts (owner, balance) VALUES ('Ada', 500), ('Linus', 100);

การย้ายเงิน 100 จาก Ada ไปยัง Linus คือการ update สองครั้งที่ต้องเกิดขึ้นพร้อมกัน ถ้า server crash ระหว่างสองคำสั่งนั้น atomicity รับประกันว่าจะไม่มีการเปลี่ยนแปลงใดอยู่รอด เงินจึงไม่ถูกสร้างขึ้นหรือหายไป

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada';
UPDATE accounts SET balance = balance + 100 WHERE owner = 'Linus';
COMMIT;

หลังจาก commit ยอดเงินคงเหลือสะท้อนการ update ทั้งสองครั้งไปด้วยกัน

id | owner | balance
----+-------+---------
1 | Ada | 400
2 | Linus | 200
(2 rows)

หากมีอะไรดูผิดปกติก่อนที่คุณจะ commit ROLLBACK จะทิ้งทุกการเปลี่ยนแปลงที่ทำไปตั้งแต่ BEGIN ราวกับว่า transaction นั้นไม่เคยรันเลย ในที่นี้ Ada มีเงินไม่พอ เราจึงทิ้งทั้งหมด

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada';
-- on reflection, cancel everything:
ROLLBACK;

ทุก transaction เดินตามเส้นทางเดียวกัน คือเริ่มต้น รันคำสั่งข้างใน แล้วจบลงทางใดทางหนึ่งในสองทางนี้เท่านั้น ไม่มีสถานะระหว่างกลางเมื่อคุณตัดสินใจแล้ว

flowchart TD
  A[BEGIN] --> B[Run statements]
  B --> C{Everything correct?}
  C -->|Yes| D[COMMIT: changes become permanent]
  C -->|No| E[ROLLBACK: changes discarded]
  D --> F[Transaction ends]
  E --> F
transaction เริ่ม รัน แล้วเลือกที่จะ commit หรือ roll back

บางครั้งคุณต้องการยกเลิกเพียงบางส่วนของ transaction โดยไม่ละทิ้งส่วนที่เหลือ SAVEPOINT คือเครื่องหมายที่มีชื่อภายใน transaction ROLLBACK TO จะย้อนกลับไปยังเครื่องหมายนั้น โดยเก็บทุกอย่างก่อนหน้าเครื่องหมายไว้ และทิ้งทุกอย่างหลังจากนั้น

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE owner = 'Ada';
SAVEPOINT after_debit;
UPDATE accounts SET balance = balance + 100 WHERE owner = 'Nobody';
-- that owner does not exist; undo just the second update:
ROLLBACK TO after_debit;
UPDATE accounts SET balance = balance + 100 WHERE owner = 'Linus';
COMMIT;

การหักเงินและการเพิ่มเงินครั้งสุดท้ายถูกเก็บไว้ มีเพียงงานระหว่าง savepoint กับ ROLLBACK TO เท่านั้นที่ถูกทิ้งไป

owner | balance
-------+---------
Ada | 400
Linus | 200
(2 rows)
  • ทำให้ transaction สั้น transaction ที่เปิดค้างไว้จะถือทรัพยากรไว้ และอย่างที่บทเรียน MVCC แสดงไว้ ยังอาจหน่วงการเก็บกวาด row เวอร์ชันเก่าด้วย
  • โดยค่าเริ่มต้น connection อยู่ในโหมด auto-commit คือแต่ละคำสั่งเดี่ยว ๆ เป็น transaction ที่ commit ของตัวเอง คุณออกจากโหมดนั้นได้ด้วยการสั่ง BEGIN เท่านั้น
  • error ภายใน transaction จะผลัก transaction เข้าสู่สถานะ aborted แล้ว PostgreSQL จะปฏิเสธคำสั่งต่อไปจนกว่าคุณจะ ROLLBACK (หรือ ROLLBACK TO ไปยัง savepoint)
  • ในโค้ดแอปพลิเคชัน ให้จับคู่ BEGIN กับ ROLLBACK บนเส้นทาง error เสมอ ตัวอย่าง driver ด้านบนแสดงรูปแบบ try-or-finally ที่รับประกันว่า connection จะไม่ปล่อย transaction ที่เปิดค้างหลุดออกไป
ตัวเลือกBenefitCost
ห่อคำสั่งที่เกี่ยวข้องกันด้วย BEGIN...COMMITatomicity รับประกันว่างานทั้งหมดสำเร็จหรือไม่มีอะไรเกิดขึ้นเลยต้องเขียนโค้ดจัดการ error path (ROLLBACK) เอง และ transaction ที่เปิดค้างจะถือ lock ไว้นานขึ้น
ปล่อยให้แต่ละคำสั่งรันแบบ auto-commitเขียนโค้ดง่ายกว่า ไม่ต้องจัดการ transaction เองถ้า crash หรือ error เกิดขึ้นกลางทาง ข้อมูลอาจไม่สอดคล้องกัน เพราะแต่ละคำสั่ง commit แยกกัน
  • ลืม COMMIT หรือ ROLLBACK แล้วปล่อย transaction ค้างไว้ — transaction ที่เปิดค้างยังถือ lock และหน่วงการเก็บกวาดของ VACUUM อยู่ ให้จับคู่ BEGIN กับ path จบเสมอ ไม่ว่าจะสำเร็จหรือ error
  • คิดว่าคำสั่งหลายคำสั่งเป็น atomic โดยอัตโนมัติ — ถ้าไม่ได้ห่อด้วย BEGIN แต่ละคำสั่งจะ commit แยกกันทันที การ crash ระหว่างคำสั่งจะทิ้งข้อมูลไว้ครึ่ง ๆ กลาง ๆ
  • ไม่จัดการ error ระหว่าง transaction — เมื่อคำสั่งหนึ่ง fail transaction จะเข้าสู่สถานะ aborted ทันที คำสั่งถัดไปทั้งหมดจะถูกปฏิเสธเงียบ ๆ จนกว่าคุณจะ ROLLBACK

💡 ตัวอย่างจากของจริง

ระบบธนาคารและ payment gateway — การโอนเงินหักบัญชีหนึ่งแล้วเพิ่มอีกบัญชีหนึ่งต้องห่อด้วย transaction เดียวเสมอ เพื่อไม่ให้ crash ระหว่างสองคำสั่งทำให้เงินหายหรือถูกสร้างขึ้นมาลอย ๆ นี่คือเหตุผลคลาสสิกที่ ACID transaction ถูกออกแบบมา

Stripe และระบบ e-commerce — เมื่อสร้างคำสั่งซื้อ ระบบต้องหักสต็อกสินค้าและบันทึก order พร้อมกันในหนึ่ง transaction ถ้าขั้นตอนใดล้มเหลว ทั้งสอง operation ต้อง roll back กลับไปพร้อมกัน ไม่ปล่อยให้สต็อกถูกหักแต่ order ไม่ถูกสร้าง

ROLLBACK ทำอะไรกับ statement ที่รันไปตั้งแต่ BEGIN?
SAVEPOINT มีไว้เพื่ออะไร?
statement เดี่ยว ๆ ที่รันแบบไม่อยู่ใน transaction จะถูก commit อัตโนมัติเมื่อไหร่?