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

Transactions & Concurrency

จนถึงตอนนี้ทุก statement ที่คุณเขียนรันเดี่ยว ๆ แต่ในแอปจริง การกระทำทางธุรกิจหนึ่งอย่างมักต้องใช้หลาย statement ที่ต้องสำเร็จพร้อมกันทั้งหมดหรือไม่ก็ไม่สำเร็จเลย การย้ายเงินระหว่างสองบัญชีเป็นตัวอย่างคลาสสิก: row หนึ่งลด อีก row เพิ่ม และคุณรับไม่ได้เลยที่จะทำแค่ครึ่งเดียว

transaction คือเครื่องมือสำหรับสิ่งนี้เอง มันจัดกลุ่ม statement หนึ่งตัวหรือมากกว่าเป็นหน่วยงาน all-or-nothing หน่วยเดียว ไม่ทุก statement มีผล ก็ไม่มีเลย นอกจากนี้ฐานข้อมูลที่ยุ่งมีหลาย transaction รันพร้อมกัน ซึ่งเราเรียกว่า concurrency โมดูลนี้ครอบคลุมทั้งสอง: วิธีกำหนดหน่วยงาน และวิธีที่ PostgreSQL ไม่ให้พวกมันทำลายข้อมูลของกันและกัน

ทุกบทเรียนในโมดูลนี้รันบน PostgreSQL 18.6 (postgres:18.6, aarch64, Docker Desktop บน Mac, กันยายน 2026) ตัวอย่าง concurrency ไม่ได้ถูกเล่า แต่ถูกทำซ้ำ: สองหรือสาม session psql จริงที่เริ่มพร้อมกัน พิมพ์ผลลัพธ์ตามที่ตอบกลับมา

transaction ใน PostgreSQL ให้การรับประกันสี่อย่าง จำด้วยตัวย่อ ACID (docs)

ตัวอักษรคุณสมบัติสัญญาว่า
AAtomicityทุก statement commit พร้อมกันหรือ rollback พร้อมกัน ไม่ทำครึ่งเดียว
CConsistencyฐานข้อมูลเปลี่ยนจากสถานะที่ถูกต้องหนึ่งไปอีกอัน โดยเคารพ constraint
IIsolationtransaction ที่พร้อมกันไม่เห็นงานที่ยังไม่เสร็จของกันและกัน
DDurabilityเมื่อ commit แล้ว การเปลี่ยนแปลงรอดจาก crash หรือไฟดับ

Atomicity คือส่วนที่คุณรู้สึกโดยตรงที่สุดเมื่อพิมพ์ COMMIT หรือ ROLLBACK Isolation คือส่วนที่น่าสนใจเมื่อมี client เชื่อมต่อมากกว่าหนึ่ง และเป็นที่ที่ความลึกส่วนใหญ่ของโมดูลนี้อยู่

สี่บทถัดไปต่อยอดกัน คุณเริ่ม transaction เลือกว่าจะแยกตัวมากแค่ไหน และใต้ทั้งสองอย่าง PostgreSQL ใช้สองกลไกให้ concurrency ปลอดภัย: เก็บหลาย version ของแต่ละ row และถือ lock เมื่อจำเป็น

flowchart TD
  A[Transaction: BEGIN to COMMIT] --> B[Isolation level: how much it sees]
  B --> C[MVCC: each transaction reads a snapshot]
  B --> D[Locking: serialize conflicting writes]
  C --> E[Safe concurrency for many clients]
  D --> E
จาก transaction เดียวสู่ concurrency ที่ปลอดภัย
  • พื้นฐาน Transaction — BEGIN, COMMIT, ROLLBACK, สิ่งที่เกิดเมื่อ statement ล้มใน transaction (25P02), SAVEPOINT สำหรับ rollback บางส่วน และ connection รั่วที่เห็นได้ใน pg_stat_activity
  • Isolation levels — Read Committed, Repeatable Read และ Serializable พร้อมทำซ้ำทุก anomaly (ยอดที่เปลี่ยนกลาง transaction, write skew ที่ commit, 40001 และ retry loop ที่จัดการมัน)
  • MVCC — Multi-Version Concurrency Control ที่ผู้เขียนไม่บล็อกผู้อ่านและผู้อ่านไม่บล็อกผู้เขียน เพราะแต่ละ transaction อ่าน snapshot ที่สอดคล้อง คุณดู xmin, xmax และ ctid และ heap page ดิบ และเห็นว่าทำไม transaction ที่ลืมไว้อันเดียวทำให้ table บวม
  • Locking และ deadlock — row lock แบบชัดเจนด้วย SELECT ... FOR UPDATE, lost update ที่มันป้องกัน, lock สี่โหมด, deadlock จริงพร้อม server log, NOWAIT, SKIP LOCKED และ advisory lock

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

-- sql/setup.sql
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);
-- sql/first-look.sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
SELECT id, owner, balance FROM accounts ORDER BY id;
BEGIN
UPDATE 1
UPDATE 1
COMMIT
id | owner | balance
----+-------+---------
1 | Ada | 400
2 | Linus | 200
(2 rows)

การโอนเดียวกันจากแต่ละ driver รันหนึ่งครั้งกับฐานข้อมูล scratch ที่รีเซ็ตเป็น Ada 500 / Linus 100 ก่อน (pg 8.23.0, psycopg 3.3.6, pgx v5.11.0, sqlx 0.9.0):

$ (each snippet run once; the scratch database is reset before every run)
[first look] Node.js (pg 8.23.0): Ada 400, Linus 200
[first look] Python (psycopg 3.3.6): Ada 400, Linus 200
[first look] Go (pgx v5.11.0): Ada 400, Linus 200
[first look] Rust (sqlx 0.9.0): Ada 400, Linus 200

ใน pgAdmin: Query Tool รันแต่ละ statement เป็น transaction ที่ auto-commit ของมันเองโดยปกติ เพื่อจัดกลุ่ม statement พิมพ์ BEGIN; เองที่ด้านบนและ COMMIT; ที่ท้าย แล้วรันทั้งบล็อกพร้อมกัน

ถือสองแนวคิดพร้อมกัน transaction คือขอบเขตของ คุณ — คุณตัดสินใจว่างานเริ่มและจบเมื่อไร concurrency control คือหน้าที่ของ PostgreSQL — มันตัดสินว่า transaction ของคุณอยู่ร่วมกับของทุกคนได้อย่างไร โดยใช้ snapshot (ผู้อ่านจึงไม่รอ) และ lock (ผู้เขียนจึงผลัดกัน) ส่วนที่เหลือของโมดูลแสดงว่าสองความรับผิดชอบนี้มาเจอกันที่ไหน และโค้ดของคุณต้องทำอะไรเมื่อ PostgreSQL บอกว่า “ลองใหม่”

  • connection ที่ไม่อยู่ใน transaction แบบชัดเจนรันแต่ละ statement ใน transaction โดยนัยของมันเอง และ commit ทันที เรียกว่า auto-commit
  • isolation คือสเปกตรัม ไม่ใช่สวิตช์ level ที่สูงขึ้นป้องกัน anomaly ได้มากขึ้น แต่ทำให้ความขัดแย้งมีโอกาส abort มากขึ้น
  • PostgreSQL ไม่เคยขอให้ผู้อ่านรอผู้เขียนสำหรับ query SELECT ธรรมดา นั่นคือประโยชน์หลักของ MVCC
  • deadlock และ serialization failure เป็นความเป็นไปได้ปกติในระบบ concurrent ไม่ใช่ bug แอปที่เขียนดีคาดหวังและ retry
  • “transaction ทำให้โค้ดช้าลง” transaction แทบไม่มีต้นทุนเมื่อสั้น ที่ทำให้เจ็บคืออันที่ ยาว (ดูบทเรียน MVCC)
  • “isolation สูงกว่าปลอดภัยกว่าเสมอ” มันเข้มกว่า และ abort มากกว่า ถ้าไม่มี retry loop นั่นคือ outage ไม่ใช่ความปลอดภัย
  • “การอ่านและการเขียนบล็อกกัน” ไม่ใช่ใน PostgreSQL: การอ่านธรรมดาใช้ snapshot และไม่ถือ row lock
"A" ใน ACID รับประกันอะไรเกี่ยวกับ transaction?
"concurrency" หมายถึงอะไรในบริบทของฐานข้อมูล?
คุณสมบัติ ACID ใดอธิบายว่า transaction ที่พร้อมกันไม่เห็นงานที่ยังไม่เสร็จของกันและกัน?
นอก BEGIN ที่ชัดเจน เกิดอะไรกับ UPDATE เดี่ยว?
SQLSTATE สองรหัสใดแปลว่า "rollback แล้ว retry ทั้ง transaction"?