Distributed Systems · บทที่ 4 จาก 11

Transactions & Concurrency Control

เมื่อหลาย client เข้าถึงข้อมูลชุดเดียวกันพร้อมกัน ผลลัพธ์ต้องยังถูกต้องเสมือนมีคนทำทีละคน — บทนี้อธิบายว่า "ถูกต้อง" หมายถึงอะไรในทางทฤษฎี (serializability) และมีกลไกอะไรบ้างที่ทำให้มันเกิดขึ้นจริง

📚
อิงเนื้อหาจาก Coulouris บทที่ 13 (Transactions and Concurrency Control) — เป็นพื้นฐานทางทฤษฎีของทุกระบบฐานข้อมูลที่ใช้งานจริงในปัจจุบัน ตั้งแต่ PostgreSQL ไปจนถึง CockroachDB

1Transaction และคุณสมบัติ ACID

นิยาม
Transaction คือกลุ่มของ operation ที่ต้องถูกมองว่าเป็นหน่วยเดียว — ทำสำเร็จทั้งหมดหรือไม่ทำเลย ไม่มีสถานะ "ทำไปครึ่งหนึ่ง" ที่คนอื่นมองเห็นได้
คุณสมบัติความหมาย
AtomicityTransaction สำเร็จทั้งหมด (commit) หรือไม่มีผลอะไรเลย (abort/rollback) ไม่มีผลลัพธ์บางส่วน
ConsistencyTransaction พาข้อมูลจากสถานะที่ถูกต้องหนึ่งไปยังอีกสถานะที่ถูกต้อง โดยไม่ละเมิด invariant ของระบบ
IsolationTransaction ที่ทำงานพร้อมกันต้องไม่เห็นสถานะกลาง ๆ ของกันและกัน — เสมือนรันทีละตัว
Durabilityเมื่อ commit สำเร็จแล้ว ผลลัพธ์ต้องอยู่ถาวรแม้ระบบล่มทันทีหลังจากนั้น

บทนี้เน้นที่ Isolation เป็นหลัก เพราะเป็นคุณสมบัติที่ซับซ้อนที่สุดในการรับประกันเมื่อมี concurrency

2ปัญหาที่เกิดขึ้นเมื่อไม่มี Concurrency Control

ปัญหาตัวอย่าง
Lost updateT1 อ่านค่า x=10, T2 อ่านค่า x=10 พร้อมกัน ทั้งคู่บวก 5 แล้วเขียนกลับ — ผลลัพธ์ควรเป็น 20 แต่กลายเป็น 15 เพราะ T2 เขียนทับ T1
Inconsistent retrieval (dirty read)T1 กำลังโอนเงินจากบัญชี A ไป B (ยังไม่ commit) T2 มาอ่านยอดรวม A+B ระหว่างนั้นพอดี อาจเห็นยอดรวมผิดชั่วขณะ หรือถ้า T1 ท้าย abort T2 ก็อ่านค่าที่ไม่เคยมีจริง
Serial Equivalence — นิยามความถูกต้อง
Schedule การรัน transaction พร้อมกันจะถือว่า "ถูกต้อง" ก็ต่อเมื่อผลลัพธ์เหมือนกับการรัน transaction เหล่านั้นทีละตัวเรียงกัน (ในลำดับใดลำดับหนึ่ง) — เรียกว่า schedule นั้น serially equivalent หรือ serializable

3Locking: กลไก Pessimistic Concurrency Control

ชนิด Lockอนุญาต
Shared lock (read lock)หลาย transaction ถือ shared lock บน object เดียวกันพร้อมกันได้ (อ่านพร้อมกันได้)
Exclusive lock (write lock)มีได้แค่ transaction เดียวเท่านั้นที่ถือ และห้ามมี lock อื่นใดค้างอยู่บน object นั้น

Two-Phase Locking (2PL)

กฎ
แบ่งช่วงชีวิตของ transaction เป็น 2 phase: Growing phase — ขอ lock ได้เรื่อย ๆ แต่ห้ามปล่อย lock เลย และ Shrinking phase — ปล่อย lock ได้เรื่อย ๆ แต่ห้ามขอ lock ใหม่อีก เมื่อปล่อย lock ตัวแรกแล้ว ถือว่าเข้าสู่ shrinking phase ทันที
Strict Two-Phase Locking (ที่ใช้จริง)
ถือ lock ทั้งหมดไว้จนกว่า transaction จะ commit หรือ abort เท่านั้น (ไม่ปล่อยก่อนหน้านั้นเลย)
ป้องกัน cascading abort — transaction อื่นจะไม่มีทางเห็นข้อมูลที่ยังไม่ commit
ผลข้างเคียงของ 2PL
2PL รับประกัน serializability ได้ แต่แลกมาด้วย Deadlock ที่เป็นไปได้เสมอเมื่อมี 2PL — ดูหัวข้อถัดไป

Deadlock: การติดตายเมื่อรอ Lock กันเป็นวงกลม

นิยาม
Deadlock เกิดเมื่อ T1 รอ lock ที่ T2 ถืออยู่ ในขณะที่ T2 ก็รอ lock ที่ T1 ถืออยู่ (หรือเป็นวงกลมที่ยาวกว่านั้น) — ไม่มี transaction ไหนไปต่อได้เลย
แนวทางรับมือวิธีการ
Deadlock Preventionออกแบบการขอ lock ให้ deadlock เกิดไม่ได้ตั้งแต่ต้น เช่น บังคับขอ lock ตามลำดับคงที่เสมอ — แต่จำกัดความยืดหยุ่นของระบบมาก
Deadlock Detectionยอมให้ deadlock เกิดขึ้นได้ แล้วตรวจจับด้วย wait-for graph (ใครรออะไรอยู่) หากพบวงกลมในกราฟ แปลว่ามี deadlock ต้อง abort transaction ตัวใดตัวหนึ่งในวงเพื่อตัดวงจร
Lock Timeoutกำหนดเวลาสูงสุดที่รอ lock ได้ ถ้าเกินเวลาถือว่าอาจติด deadlock แล้ว abort ทิ้งไปเลย — ใช้งานง่ายแต่ไม่แม่นยำ (อาจ abort transaction ที่ไม่ได้ deadlock จริง แค่รอนาน)

4Optimistic Concurrency Control (OCC)

แนวคิดตรงข้ามกับ Locking
แทนที่จะป้องกัน conflict ล่วงหน้าด้วย lock (pessimistic) OCC ปล่อยให้ transaction ทำงานไปก่อนโดยไม่ล็อกอะไรเลย แล้วค่อยตรวจสอบตอนจบว่ามี conflict เกิดขึ้นจริงหรือไม่ — เหมาะกับสถานการณ์ที่ conflict เกิดขึ้นไม่บ่อย (การล็อกล่วงหน้าจะเสียเวลาโดยเปล่าประโยชน์)
Phaseการทำงาน
1. Working phaseTransaction อ่าน/เขียนข้อมูลบนสำเนาชั่วคราวของตัวเอง (tentative version) ไม่กระทบข้อมูลจริง
2. Validation phaseตรวจสอบว่า operation ที่ทำไปขัดแย้งกับ transaction อื่นที่ commit ไปแล้วระหว่างนี้หรือไม่
3. Update phaseถ้า validate ผ่าน เขียนผลลัพธ์จริงลงระบบ (commit) ถ้าไม่ผ่าน abort แล้วเริ่มใหม่
รูปแบบ Validationวิธีตรวจสอบ
Backward validationเทียบชุดข้อมูลที่ transaction นี้อ่าน/เขียน กับ transaction ที่ commit ไปแล้วก่อนหน้า — มองย้อนกลับไปในอดีต
Forward validationเทียบกับ transaction ที่ ยังทำงานอยู่ในขณะนี้ (concurrent, ยังไม่ commit) — มองไปข้างหน้า

5Timestamp Ordering

แนวคิด
แต่ละ transaction ได้รับ timestamp ไม่ซ้ำกันตอนเริ่มทำงาน (เรียงตามลำดับเวลาที่เริ่ม) — การเข้าถึงข้อมูลจะถูกกำหนดให้เกิดขึ้นตามลำดับ timestamp เสมือนว่า transaction ถูก serialize ไว้ล่วงหน้าตามลำดับนั้น ถ้า operation ใดขัดกับลำดับนี้ (เช่น transaction ที่ timestamp ใหม่กว่าพยายามเขียนทับค่าที่ transaction เก่ากว่ายังไม่ได้อ่าน) transaction นั้นจะถูก abort ทันที

6ทันสมัย: MVCC และ Isolation Level ในฐานข้อมูลปัจจุบัน

Multi-Version Concurrency Control (MVCC)
ฐานข้อมูลสมัยใหม่ส่วนใหญ่ (PostgreSQL, MySQL InnoDB, Oracle) ไม่ได้ใช้ locking ล้วน ๆ ตามที่อธิบายข้างต้น แต่เก็บหลายเวอร์ชันของแถวข้อมูลไว้พร้อมกัน — แต่ละ transaction อ่านเวอร์ชันที่ "ตรงกับเวลาที่มันเริ่ม" แทนที่จะต้องรอ lock ของนักเขียนคนอื่น ทำให้ read ไม่ต้อง block เขียน (และเขียนไม่ต้อง block read) ซึ่งคือวิวัฒนาการของแนวคิด timestamp ordering ผสมกับ OCC
Isolation Level (SQL Standard)อนุญาตปัญหาอะไรได้บ้าง
Read UncommittedDirty read ได้ (แทบไม่ใช้จริงในปัจจุบัน)
Read Committedป้องกัน dirty read แต่ non-repeatable read ยังเกิดได้ (อ่านค่าเดิมสองครั้งได้ค่าไม่เท่ากัน)
Repeatable Read / Snapshot Isolationอ่านเห็นภาพข้อมูล ณ จุดเริ่ม transaction เสมอ แต่ phantom read บางกรณียังเกิดได้
Serializableรับประกัน serializability เต็มรูปแบบ ตรงกับทฤษฎีในหัวข้อที่ 2 — ราคาคือประสิทธิภาพต่ำสุดในบรรดา isolation level ทั้งหมด
ข้อสังเกตสำคัญ
ฐานข้อมูลกระจายสมัยใหม่อย่าง Google Spanner และ CockroachDB ยังต้องแก้ปัญหาเดียวกับที่บทนี้อธิบาย (serializability, concurrency control) เพียงแต่ต้องทำข้ามหลายเครื่อง — นั่นคือเนื้อหาของบทถัดไป (Distributed Transactions) ที่ยกระดับปัญหาในบทนี้ให้ข้ามเครือข่าย
คำถามซ้อมสอบ
  1. อธิบายปัญหา lost update และ dirty read พร้อมยกตัวอย่างที่ต่างจากในบทเรียน
  2. อธิบายว่า serial equivalence เป็นเกณฑ์ตัดสิน "ความถูกต้อง" ของ schedule อย่างไร
  3. เปรียบเทียบ pessimistic concurrency control (locking) กับ optimistic concurrency control ว่าเหมาะกับสถานการณ์แบบใดต่างกัน
  4. อธิบายว่าทำไม Two-Phase Locking จึงนำไปสู่ความเป็นไปได้ของ deadlock เสมอ และมีวิธีรับมือ deadlock กี่แบบ
  5. อธิบายว่า MVCC ในฐานข้อมูลสมัยใหม่ผสมผสานแนวคิดจาก timestamp ordering และ optimistic concurrency control อย่างไร

7Transaction รักษา Invariant ไม่ใช่เพียงรวมคำสั่งหลายบรรทัด

คนมักอธิบาย transaction ว่า “ทำทั้งหมดหรือไม่ทำเลย” ซึ่งครอบ Atomicity แต่ยังไม่อธิบายว่าทำไปเพื่ออะไร แก่นจริงคือการรักษา invariant หรือเงื่อนไขที่ต้องเป็นจริงของข้อมูล เช่นยอดเงินรวมไม่หาย ที่นั่งหนึ่งที่ไม่ขายให้สองคน และ stock ไม่ติดลบโดยไม่มีนโยบายรองรับ

Consistency ใน ACID ไม่ได้หมายถึง replica ทุกชุดเห็นข้อมูลเหมือนกันแบบ distributed consistency แต่หมายถึง transaction ที่ถูกต้องพาระบบจาก state ที่ invariant เป็นจริงไปยังอีก state ที่ invariant ยังเป็นจริง ฐานข้อมูลช่วยด้วย constraint และ isolation แต่ business invariant ที่ไม่ประกาศให้ระบบรู้ยังเป็นหน้าที่ของ application

Metaphor: ห้องเปลี่ยนชุด
Transaction คล้ายห้องเปลี่ยนชุด คนภายนอกควรเห็นเราก่อนเข้าและหลังออก ไม่ใช่เห็นขั้นกลางทุกชิ้น Atomicity บอกว่าเปลี่ยนครบหรือกลับชุดเดิม Isolation บอกว่าคนอื่นไม่เข้ามาแก้ชุดพร้อมกัน Durability บอกว่าหลังออกจากห้องแล้วผลไม่หายเมื่อไฟดับ แต่ metaphor นี้มีขอบเขต: database ยังมี isolation level หลายระดับที่ยอมให้เห็นบางปรากฏการณ์เพื่อ performance
Invariantตัวอย่าง operationกลไกที่ช่วย
ยอดรวม A+B คงเดิมโอนเงินtransaction + row lock/serializable
email ไม่ซ้ำสมัครสมาชิกพร้อมกันunique constraint ไม่ใช่ check ในโค้ดอย่างเดียว
stock ≥ 0ตัด stock หลาย orderatomic conditional update/version
หนึ่ง idempotency key ต่อหนึ่งผลretry ชำระเงินunique key ผูกกับ business commit

Application check ที่ดูถูกแต่แข่งกันได้

if stock >= quantity:
    stock = stock - quantity
    save(stock)

โค้ดนี้ถูกในโลกทีละ request แต่ผิดเมื่อสอง transaction อ่าน stock เดิมพร้อมกัน การตรวจแล้วเขียนเป็นสองเหตุการณ์ ถ้าไม่มี lock, version check หรือ atomic SQL update ทั้งคู่ผ่านเงื่อนไขได้ การเขียนโค้ดที่ “อ่านง่าย” ไม่ได้ทำให้ schedule ถูกต้อง

8จาก Interleaving ไปสู่ Serializability

Concurrency ทำให้ operation ของหลาย transaction สลับกันได้ เราไม่จำเป็นต้องบังคับให้รันทีละ transaction เพราะจะเสีย parallelism เป้าหมายคืออนุญาต interleaving ที่ผลเทียบเท่ากับ serial order บางลำดับ สิ่งนี้เรียกว่า serializability

Operation conflict เมื่อมาจากคนละ transaction เข้าถึง item เดียวกัน และอย่างน้อยหนึ่ง operation เป็น write: read–write, write–read และ write–write ส่วน read–read สลับกันได้โดยไม่เปลี่ยนผล

T1: R(A) W(A)       T2: R(A) W(A)

Schedule S: R1(A), R2(A), W1(A), W2(A)

ถ้าทั้งคู่เพิ่มค่า A จาก 10 คนละ 5 ต่างอ่าน 10 และเขียน 15 ผลสุดท้ายเป็น 15 แทน 20 นี่คือ lost update Schedule ไม่เทียบเท่ากับการรัน T1→T2 หรือ T2→T1 ซึ่งควรได้ 20

Precedence/Serialization Graph

สร้าง node ต่อ transaction และวาด edge Ti→Tj ถ้า operation ของ Ti มาก่อนและ conflict กับ Tj บน item เดียวกัน ถ้ากราฟไม่มี cycle schedule เป็น conflict-serializable และ topological order ให้ serial order ที่เทียบเท่า ถ้ามี cycle ไม่มีทางสลับ non-conflicting operation ให้กลายเป็น serial schedule ได้

  1. เขียน schedule พร้อม transaction ID และ data item
  2. หา conflict ทุกคู่ตามลำดับที่เกิด
  3. วาด edge จาก operation ก่อนหน้าไป transaction หลัง
  4. ตรวจ cycle และอธิบาย ไม่ใช่เดาจากหน้าตาว่าสลับเยอะ

Serializability ไม่เท่ากับลำดับเวลาจริงเสมอ

Serializability ต้องการผลเทียบเท่า serial order หนึ่ง แต่ไม่ได้บังคับว่า transaction ที่เริ่มหรือจบก่อนในโลกจริงต้องมาก่อนเสมอ Strict serializability เพิ่ม real-time order เข้าไป ถ้า T1 จบก่อน T2 เริ่ม ทุกคนต้องเห็น T1 ก่อน T2 แนวคิดนี้สำคัญใน distributed database ที่ต้องอธิบาย guarantee ต่อผู้ใช้

9Anomaly มากกว่า Dirty Read และ Lost Update

Anomalyเหตุการณ์ผลที่เห็น
Dirty readอ่านค่าที่ยังไม่ commitใช้ค่าที่อาจถูก rollback
Non-repeatable readอ่าน row เดิมสองครั้งแต่มีคน commit เปลี่ยนกลางทางtransaction เดียวเห็นค่าไม่เท่ากัน
Phantomquery ตาม predicate สองครั้ง มี row เข้า/ออกชุดจำนวนผลลัพธ์เปลี่ยน
Lost updatewrite ใหม่ทับผลของอีก transaction จากค่าเก่าการแก้หนึ่งชุดหาย
Write skewสอง transaction อ่าน snapshot เดียวและเขียนคนละ rowแต่ละ row ไม่ conflict ตรง ๆ แต่ invariant รวมเสีย
Read skewอ่านข้อมูลสัมพันธ์กันจากคนละเวลาเห็น state ที่ไม่เคยอยู่พร้อมกัน

Write Skew: กับดักของ Snapshot Isolation

สมมุติต้องมีแพทย์เวรอย่างน้อยหนึ่งคน Alice และ Bob อยู่เวร ทั้งคู่ต้องการลา แต่ละ transaction อ่าน snapshot ว่าอีกคนยังอยู่จึงแก้ row ของตนเป็นลา ทั้งคู่เขียนคนละ row ไม่มี write–write conflict จึง commit ได้ ผลสุดท้ายไม่มีใครอยู่เวรและ invariant เสีย

Snapshot Isolation ป้องกัน anomaly จำนวนมากและให้ read concurrency ดี แต่ไม่เท่ากับ serializable ทุกกรณี การป้องกัน write skew อาจใช้ serializable isolation, explicit lock บนสิ่งแทน invariant, materialized constraint หรือออกแบบข้อมูลให้ conflict บน row เดียวกัน

10Locking แบบที่ระบบจริงต้องตัดสินใจ

Shared/Exclusive lock เป็นจุดเริ่ม แต่ระบบจริงต้องเลือก granularity ถ้าล็อกทั้ง table overhead ต่ำแต่ concurrency ต่ำ ถ้าล็อกทีละ row concurrency สูงแต่มี lock จำนวนมาก และ predicate/range query อาจต้องป้องกัน phantom ด้วย range/index lock

Granularityข้อดีข้อเสีย
Database/Tableจัดการง่าย lock น้อยblock งานที่ไม่ conflict จริงจำนวนมาก
Page/Partitionสมดุลบาง workloadrow คนละตัวใน page เดียวกันยังขวางกัน
Rowconcurrency สูงmemory/management overhead และ deadlock ซับซ้อน
Predicate/Rangeป้องกัน phantom ตามเงื่อนไข queryimplementation และ reasoning ยากขึ้น

Two-Phase Locking กับ Strictness

Basic 2PL รับประกัน conflict serializability โดยมี growing/shrinking phase แต่ถ้าปล่อย write lock ก่อน commit transaction อื่นอาจอ่านค่าที่ยัง rollback ได้และเกิด cascading abort Strict 2PL ถือ exclusive lock ถึง commit/abort ส่วน rigorous 2PL ถือทั้ง shared และ exclusive ถึงจบ ทำ recovery reasoning ง่ายขึ้นแต่เพิ่มเวลาถือ lock

Deadlock เป็นผลจากนโยบายรอ ไม่ใช่ Bug แปลกประหลาด

ถ้า T1 lock A รอ B และ T2 lock B รอ A เกิดวงจรใน wait-for graph แนวทางมีสามกลุ่ม: ป้องกันด้วย ordering/preclaim, หลีกเลี่ยงด้วย timestamp policy เช่น wait-die/wound-wait หรือยอมให้เกิดแล้ว detect cycle/เลือก victim

วิธีหลักผลข้างเคียง
Global lock orderทุกคนขอ A ก่อน Bต้องรู้ชุด lock/order และอาจลด flexibility
Wait-dietransaction เก่ารอ ใหม่ abort เมื่อชนเก่าลด cycle แต่ transaction ใหม่อาจ restart
Wound-waitเก่าบังคับใหม่ abort ใหม่รอเก่าให้ priority งานเก่า
Detectionสร้าง wait-for graph หา cycleมี overhead และต้องเลือก victim
Timeoutรอนานแล้ว abortง่ายแต่ false positive และแก้ช้าถ้าค่ายาว

การเลือก victim ควรดู work ที่ทำไป จำนวน lock, rollback cost, อายุ และ starvation Transaction ที่ถูก abort ซ้ำต้องมี priority/backoff ไม่เช่นนั้นระบบ technically เดินหน้าได้แต่ผู้ใช้รายหนึ่งไม่มีวันสำเร็จ

11Optimistic Concurrency Control: เดิมพันว่า Conflict น้อย

OCC ให้ transaction ทำงานบน tentative state แล้ว validate ก่อน commit จึงไม่มี lock wait ระหว่าง working phase เหมาะกับ read-heavy, transaction สั้น และ conflict ต่ำ แต่เมื่อ contention สูง งานจำนวนมากทำเกือบเสร็จแล้ว abort ทำให้ทรัพยากรถูกใช้ซ้ำ

คำว่า optimistic ไม่ได้แปลว่ามองโลกดี แต่เป็นสมมติฐานเชิง workload ว่า conflict rare พอให้ตรวจทีหลังคุ้ม ส่วน pessimistic ยอมจ่าย waiting ล่วงหน้าเพื่อไม่ให้ทำงานที่ต้องทิ้ง ไม่มีวิธีใดดีกว่าโดยไม่รู้ conflict rate และ cost ของ retry

Workloadแนวทางที่มักเหมาะเหตุผล
อ่านมาก เขียนคนละ keyOCC/MVCCลดการ block ที่ไม่จำเป็น
hot counter/key เดียวlock/atomic operation/partition redesignOCC จะ abort ซ้ำสูง
transaction ยาว มี user think timeversion check/short transactionห้ามถือ DB lock ระหว่างรอคน
invariant สำคัญหลาย rowserializable/explicit invariant locksnapshot อาจ write skew

Version Column ใน Web Application

UPDATE documents
SET body = :new_body, version = version + 1
WHERE id = :id AND version = :version_read;

ถ้า affected rows เป็น 0 แปลว่ามีคนแก้หลังจากเราอ่าน Application ต้องเลือกแจ้ง conflict, merge หรือโหลดใหม่ ไม่ควรเขียนทับเงียบ ๆ นี่คือ OCC แบบที่เห็นได้ใน API ด้วย ETag/If-Match

12Timestamp Ordering และเวลาที่ไม่ได้แปลว่านาฬิกาผนัง

Timestamp ordering กำหนด serial order จาก timestamp ของ transaction แล้วตรวจ read/write ให้ไม่ละเมิดลำดับ Timestamp ในที่นี้เป็น logical order ได้ ไม่จำเป็นต้องเป็นเวลาจริง ทุก data item เก็บ read timestamp และ write timestamp เพื่อรู้ว่า operation ใหม่มาช้าหรือเร็วเกินลำดับที่กำหนด

ข้อดีคือไม่มี deadlock เพราะ transaction ไม่รอ lock แต่ conflict ทำให้ abort และ restart ได้มาก โดยเฉพาะ transaction เก่าที่ช้าถูก transaction ใหม่แซง Thomas Write Rule อาจละทิ้ง obsolete write บางกรณีโดยไม่ abort แต่ต้องวิเคราะห์ semantics ให้ถูก

13MVCC: หลายเวอร์ชันเพื่อให้การอ่านไม่ขวางการเขียน

MVCC เก็บ row หลายเวอร์ชันพร้อม metadata ว่า version ใดมองเห็นสำหรับ snapshot ของ transaction Reader เลือก version ที่เหมาะแทนรอ writer ทำให้ read concurrency สูง แต่ไม่ได้แปลว่าไม่มี lock เลย Writer ยัง conflict กัน และ metadata/version เก่าต้อง vacuum/garbage collect

ประโยชน์ต้นทุนที่ซ่อน
consistent snapshotversion storage และ visibility check
reader ไม่ block writer หลายกรณีlong transaction กัก version เก่า ทำให้ bloat
rollback ใช้ versioncleanup และ transaction ID wraparound บางระบบ
replica/read scalingreplication lag ทำ snapshot เก่าอีกชั้น

Isolation Level เป็น Contract ไม่ใช่ปุ่มความเร็ว

ระดับต่ำกว่ายอม anomaly บางชนิดเพื่อ concurrency แต่ application ต้องรับผลได้ อย่าเลือก Read Committed เพราะ default แล้วสมมุติ transaction ทั้งหมด serializable ในทางกลับกัน Serializable อาจ abort transaction ด้วย serialization failure ซึ่ง application ต้อง retry ทั้ง transaction อย่างถูกต้อง

ชื่อ level มีรายละเอียดต่างกันตาม database เช่น Repeatable Read ของ PostgreSQL ใกล้ snapshot isolation ขณะที่ implementation อื่นต่างออกไป ต้องอ่านเอกสารระบบจริงและทดสอบ anomaly ไม่ใช้ชื่อมาตรฐานอย่างเดียวอนุมาน guarantee

14Concurrency Control ข้าม Process กับข้าม Service

บทนี้เน้น database/site เดียวเป็นฐาน ก่อนบท Distributed Transactions แต่ application สมัยใหม่มักเผลอแบ่ง invariant ข้าม service เช่น Order กับ Inventory อยู่คนละฐานข้อมูล Lock ในฐานหนึ่งไม่ครอบอีกฐาน การเรียกสอง API ต่อกันจึงไม่เป็น transaction เดียวโดยธรรมชาติ

ถ้าต้อง consistency ทันที อาจใช้ distributed transaction/2PC โดยยอม coordination และ availability cost ถ้ายอมขั้นกลางได้ ใช้ Saga/compensation แต่ต้องนิยาม state intermediate และ action ที่ย้อนกลับไม่ได้ สิ่งสำคัญคือไม่เรียก workflow หลาย service ว่า ACID transaction หากไม่มี protocol ที่รับประกันจริง

ตัวอย่าง
Order สร้างสำเร็จแต่ Inventory reservation ล้ม ระบบอาจเก็บ Order เป็น PENDING แล้ว retry/ยกเลิกภายหลัง นี่ไม่ใช่ Atomicity แบบฐานข้อมูล แต่เป็น state machine ที่เปิดเผย intermediate state และทำให้ convergent ในที่สุด การออกแบบ UI และ audit ต้องยอมรับโลกนี้ด้วย

15Workshop: ทำให้ Anomaly เกิดก่อนแก้

ใช้ PostgreSQL/MySQL หรือฐานข้อมูลที่เข้าถึงได้ เปิดสอง session และทำการทดลองทีละขั้น การเห็น anomaly จริงช่วยแยกสิ่งที่ฐานข้อมูลรับประกันจากสิ่งที่เราคิดว่า transaction ควรทำ

  1. สร้างบัญชี A/B และตารางแพทย์เวร พร้อม constraint พื้นฐาน
  2. ทำ lost update ด้วย read–compute–write จากสอง session
  3. แก้ด้วย atomic update และเปรียบเทียบกับ SELECT FOR UPDATE
  4. ทดลอง non-repeatable read ภายใต้ Read Committed
  5. ทดลอง snapshot เดิมภายใต้ Repeatable Read
  6. สร้าง write skew แพทย์เวร แล้วทดลอง Serializable
  7. สร้าง deadlock โดย lock A/B สลับลำดับ จด error และ victim
  8. เพิ่ม version column ทำ OCC แล้วทดสอบ conflict
  9. วัด workload conflict ต่ำ/สูง เปรียบเทียบ waiting กับ abort rate
สิ่งที่เก็บเหตุผล
SQL พร้อมลำดับเวลาสร้าง schedule ที่ตรวจซ้ำได้
ค่าที่แต่ละ session อ่านเห็น local view ต่างกัน
commit/abort/errorรู้ว่า database เลือกรักษา guarantee อย่างไร
lock/wait informationเชื่อมอาการช้ากับ contention
final invariantperformance ไม่มีความหมายถ้าข้อมูลผิด

16แนวทางเลือกกลไกโดยไม่เริ่มจากความชอบ

  1. เขียน invariant และ anomaly ที่ยอมรับไม่ได้ก่อน
  2. ระบุขอบเขตข้อมูล: row, table, database หรือหลาย service
  3. วัด read/write ratio, conflict distribution, transaction duration และ hot key
  4. เลือก isolation/control ที่ป้องกัน anomaly ด้วยต้นทุนต่ำสุดที่รับได้
  5. ออกแบบ retry สำหรับ deadlock/serialization failure ให้ทั้ง transaction และ idempotent
  6. ทดสอบ concurrent schedule ไม่ใช่ unit test ทีละ request
  7. monitor lock wait, abort rate, long transaction, version bloat และ tail latency
ข้อควรระวัง
การเพิ่ม lock แก้ correctness อาจสร้าง latency และ deadlock การลด isolation แก้ throughput อาจย้ายความซับซ้อนไป application และการใช้ OCC แก้ waiting อาจสร้างงานทิ้งจำนวนมาก ทุกวิธีเปลี่ยนชนิดต้นทุน ไม่ได้ลบต้นทุน

17สรุปและขั้นตอนถัดไป

Concurrency Control เริ่มจาก invariant และ schedule ไม่ใช่เริ่มจากเลือก lock ACID แยกความรับผิดชอบหลายด้าน โดย Isolation ทำให้ผลของงานพร้อมกันมีความหมายเสมือน serial order ระดับหนึ่ง แต่ isolation level ที่ต่ำกว่า Serializable ยอม anomaly บางชนิดและ application ต้องรู้

Locking จ่ายด้วย waiting/deadlock, OCC จ่ายด้วย validation/abort, timestamp ordering จ่ายด้วย restart เมื่อผิดลำดับ และ MVCC จ่ายด้วยหลายเวอร์ชัน/cleanup การเลือกต้องสัมพันธ์กับ contention และ invariant ระบบที่เร็วเพราะไม่ตรวจ conflict อาจเพียงผลิตข้อมูลผิดได้เร็วขึ้น ซึ่งไม่นับเป็น scalability ที่น่าภูมิใจนัก

ขั้นตอนถัดไปคือ Distributed Transactions เมื่อ atomic action ต้องข้ามหลาย process และหลายฐานข้อมูล เราจะพบว่า lock และ commit ภายใน site เดียวไม่พอ ต้องมี protocol ให้ participant ตกลงผลร่วมกัน ทั้งที่ coordinator หรือ network อาจล้มกลางทาง นั่นคือจุดที่ correctness เริ่มแลกกับ availability และเวลารออย่างชัดเจนขึ้น