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

Concurrency

บท​ก่อนๆ เรา​เก็บ Order ทั้ง​ก้อน​ลง PostgreSQL ได้​อย่าง​ซื่อสัตย์​ต่อ model แล้ว — strongly-typed ID กลาย​เป็น column ผ่าน Value Converter, Money ฝัง​เป็น owned type, _lines ยัง​ปิด​สนิท​ด้วย backing field, schema วิวัฒน์​ได้​ด้วย migration และ repository ที่​เรา​ใช้​มา​ตลอด​ก็​เป็น​แค่ wrapper บางๆ รอบ DbSet<Order> แต่​ทุก​อย่าง​ที่​ผ่าน​มา​สมมติ​เงียบๆ ว่า​มีคน​เดียวแก้​ออเดอร์​ใบ​หนึ่ง​ใน​เวลา​หนึ่ง โลก​จริง​ไม่​เป็น​แบบ​นั้น

พนักงาน​สอง​คน​เปิด​ออเดอร์​ใบ​เดียวกัน​พร้อม​กัน สอง​แท็บ​ของ app เดียวกัน​กด​ยืนยัน​ไล่หลัง​กัน​เสี้ยว​วินาที หรือ background worker กับ request ของ​ผู้​ใช้​ชน​กัน​พอดี คำถาม​ของ​บท​นี้​คือ: เมื่อ​สอง SaveChanges เขียน Order ใบ​เดียวกัน ใคร​ชนะ และ​อีก​คน​รู้ตัว​ไหม​ว่า​เพิ่ง​ทับ​งาน​คน​อื่น​ไป ถ้า​ตอบ​ว่า “คน​เซฟ​ทีหลัง​ชนะ และ​ไม่มี​ใคร​รู้​เลย” นั่น​คือ bug ที่​ทำ​เงิน​หาย​ได้​จริง

📦 code ตัวอย่าง (กำลัง​จัด​ทำ)

code เต็ม​ของ​คอร์ส​นี้​อยู่​ที่ repo kaen-food-ordering (กำลัง​จัด​ทำ) — การ​ตั้ง concurrency token อยู่​ใน FoodOrdering.Infrastructure/Persistence/FoodOrderingDbContext.cs และ​การ​จับ conflict อยู่​ใน EfOrderRepository

สันนิษฐาน​ว่า​ไม่​ค่อย​ชน แทนที่​จะ​ล็อก​ไว้​ก่อน

หัวข้อ​ที่​มีชื่อ​ว่า “สันนิษฐาน​ว่า​ไม่​ค่อย​ชน แทนที่​จะ​ล็อก​ไว้​ก่อน”

มี​สอง​วิธี​หลัก​ใน​การ​คุม​การ​เขียน​พร้อม​กัน วิธี​แรก​คือ pessimistic concurrency — ล็อก​แถว​ไว้​ตั้งแต่​ตอน​อ่าน ใคร​จะ​แตะ​ต้อง​รอ​จนกว่า​เรา​จะ​เซฟ​เสร็จ ปลอดภัย​แต่​แพง: แถว​ถูก​ล็อก​ค้าง​ระหว่าง​ที่​มนุษย์​ยัง​คิด​อยู่​หน้า​จอ ทำให้​คำขอ​อื่น​รอ​เป็น​แถว​ยาว​และ​เสี่ยง deadlock

บท​นี้​ใช้​ทาง​ที่​สอง​ซึ่ง​เหมาะ​กับ​เว็บ app ส่วน​ใหญ่: Optimistic ConcurrencyOptimistic Concurrencyกลยุทธ์​คุม​การ​เขียน​พร้อม​กัน​ที่ 'สันนิษฐาน​ว่า​ไม่​ค่อย​ชน' — ไม่​ล็อก​แถว​ไว้​ล่วงหน้า แต่​ตรวจ​ตอน SaveChanges ว่า​แถว​ยัง​เป็น​ค่าที่​อ่าน​มา​หรือ​ไม่ ถ้า​มี​คน​อื่น​แก้​ไป​ก่อน​แล้ว​จะ​โยน DbUpdateConcurrencyException แทนที่​จะ silently เขียน​ทับArchitectureสันนิษฐาน​ว่าการ​ชน​กัน​เกิด​ขึ้น​ไม่​บ่อย จึง​ไม่​ล็อก​อะไร​ไว้​ล่วงหน้า​เลย ปล่อย​ให้​ทุก​คน​อ่าน​และ​แก้​ใน​สำเนา​ของ​ตัวเอง​ได้​อิสระ แล้ว​ค่อย​ไปตรวจ​ตอน​จะ​เขียน​จริงว่า​แถว​ยัง​เป็น​ค่า​เดียว​กับ​ตอน​ที่​เรา​อ่าน​มา​หรือ​เปล่า ถ้า​ยัง — เขียน​ได้ ถ้า​ไม่ใช่ — แปล​ว่า​มี​คน​แก้​ตัด​หน้า​ไป​แล้ว EF Core จะ​ไม่​ยอม​เขียน​ทับ​เงียบๆ แต่​จะ​โยน exception ออก​มา​ให้​เรา​ตัดสิน​ใจ​แทน กลไก​ที่​ทำให้ “ตรวจ​ตอน​เขียน” เป็น​ไป​ได้​คือ concurrency token

Concurrency TokenConcurrency Tokencolumn (เช่น rowversion หรือ field version) ที่ EF Core ผูก​ไว้​กับ Optimistic Concurrency แล้ว​ใส่​ใน WHERE clause ตอน UPDATE/DELETE — ถ้า​ค่า​ไม่​ตรง​กับ​ตอน​อ่าน​มา แปล​ว่า​มี​ธุรกรรม​อื่น​แก้ Order ไป​แล้วArchitecture คือ column ที่​เก็บ “version ปัจจุบัน​ของ​แถว” และ​จะ​เปลี่ยน​ค่า​ทุก​ครั้ง​ที่​แถว​ถูก​แก้ EF Core จะ​จำ​ค่า​นี้​ไว้​ตอน​อ่าน Order เข้า​มา แล้วใส่​มัน​ลง​ใน​เงื่อนไข WHERE ตอน UPDATE/DELETE ถ้า​มี​ธุรกรรม​อื่น​แก้​แถว​ไป​ก่อน ค่า version ใน​ฐาน​ข้อมูล​จะ​ไม่​ตรง​กับ​ค่าที่​เรา​ถือ​อยู่ UPDATE จึง​จับ​ได้ 0 แถว — และ​นั่น​คือ​สัญญาณ​ของ​การ​ชน

ที่มา​ของ “ค่า version” ต่าง​กัน​ตาม​ฐาน​ข้อมูล บน​คอร์ส​นี้​ที่​ใช้ PostgreSQL + Npgsql เรา​มี​ของ​ฟรี​ที่ Postgres แถม​มา​ให้​ทุก​แถว​อยู่​แล้ว: system column ชื่อ xmin ซึ่ง​เก็บ transaction id ที่​แก้​แถว​นั้น​ล่าสุด ทุก​ครั้ง​ที่​แถว​ถูก UPDATE ค่า xmin จะ​เปลี่ยน​เอง​โดย​อัตโนมัติ — เหมาะ​เป็น concurrency token อย่าง​ยิ่ง​เพราะ​ไม่​ต้อง​เพิ่ม column ไม่​ต้อง​เขียน MigrationMigrationfile C# ที่ EF Core generate จาก diff ระหว่าง model ปัจจุบัน​กับ Model Snapshot ก่อนหน้า อธิบาย​การ​เปลี่ยน schema เป็น Up/Down ที่​ควบคุม version ได้ และ apply กับ​ฐาน​ข้อมูล​จริง​ตาม​ลำดับArchitecture ใหม่ และ​ไม่​ต้อง​แตะ domain แม้แต่​บรรทัด​เดียว (Order ยัง​ไม่รู้จัก EF Core เหมือน​เดิม รักษา Persistence Ignorance ไว้​ครบ)

ก่อน​ไป​ดู mapping ขอ​ทวน Order รูป​สุดท้าย​ที่​เรา​จะ​เก็บ — ตัว​เดิม​จาก​คอร์ส​ก่อน ไม่มี​การ re-model:

// FoodOrdering.Domain/Orders/ (ทวนจาก Course A บทที่ 2 + Course B บทที่ 2–3 —
// OrderLine เคยเป็น record ใน Course A แล้วถูกยกระดับเป็น entity ใน Course B; นี่คือรูปสุดท้าย)
public sealed record OrderId(Guid Value);
public sealed record OrderLineId(Guid Value); // ประกาศก่อนใช้ในบรรทัดถัดไป
public sealed record ProductId(Guid Value);
public sealed class OrderLine : Entity<OrderLineId> // entity class ไม่ใช่ record
{
public ProductId ProductId { get; }
public Money UnitPrice { get; }
// ...
}
public sealed class Order
{
public OrderId Id { get; } // ประตูเดียว/identity ของ aggregate
public Money Total { get; private set; } // คำนวณจากทุกบรรทัด ไม่ set อิสระ
public OrderStatus Status { get; private set; }
public IReadOnlyCollection<OrderLine> Lines { get; }
public static Order Place(OrderId id, IReadOnlyList<OrderLine> items); // invariant: ตะกร้าห้ามว่าง
public void Confirm(); // Placed -> Confirmed เท่านั้น แล้วปล่อย OrderConfirmed
}

การ​ตั้ง concurrency token เป็น​งาน​ของ​ชั้น mapping ล้วนๆ — เพิ่ม​บรรทัด​เดียว​ใน OnModelCreating ต่อยอด​จาก​ที่ Course A บท4 วาง OwnsOne(o => o.Total) / HasMany(_lines) / private-ctor rehydration ไว้​ให้​แล้ว (บท​นี้​ไม่​ทวน​ของ​เดิม ต่อยอด​ขึ้น​ไป​เลย):

✅ FoodOrderingDbContext: เพิ่ม​หน่วย​เฝ้า version ให้​ราก Order
// FoodOrdering.Infrastructure/Persistence/FoodOrderingDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(order =>
{
// ...OwnsOne(Total) / HasMany(Lines) / backing field _lines จาก Course A บท4 อยู่ตรงนี้แล้ว...
// PostgreSQL: ใช้ system column xmin ที่มีอยู่แล้วทุกแถวเป็น concurrency token
// ไม่ต้องเพิ่ม column ไม่ต้องเขียน migration และไม่ต้องแตะ domain เลย
order.UseXminAsConcurrencyToken();
// ถ้าใช้ SQL Server แทน Npgsql จะประกาศ column rowversion เป็น shadow property แทน:
// order.Property<byte[]>("Version").IsRowVersion();
// กลไกเหมือนกันเป๊ะ ต่างแค่ที่มาของค่า version (xmin ของ Postgres vs rowversion ของ SQL Server)
});
}

UseXminAsConcurrencyToken() ทำให้​ทุก UPDATE ต่อ​แถว Orders ถูก​เติม​เงื่อนไข AND xmin = @เวอร์ชันตอนอ่าน ให้​อัตโนมัติ ความ​ต่าง​ระหว่าง​มี​กับ​ไม่มี token เห็น​ชัด​ที่ SQL ที่ Change TrackingChange Trackingกลไก​ของ DbContext ที่​จด snapshot ค่า​ตอน entity ถูก​โหลด​เข้า​มา แล้ว​เทียบ​กับ​ค่า​ปัจจุบัน​ตอน SaveChanges เพื่อ​สร้าง UPDATE เฉพาะ column ที่​เปลี่ยน​จริง — มี​ต้นทุน จึง​ควร​ใช้ AsNoTracking() กับ query ที่​ไม่​ได้​ตั้งใจ​แก้ไขArchitecture generate ออก​มา:

❌ version ดิบ (ไม่มี token) เทียบ​กับ ✅ มี concurrency token
-- ❌ version ดิบ: ไม่มี concurrency token → UPDATE ไม่มีเงื่อนไข version
UPDATE "Orders" SET "Status" = 1 WHERE "Id" = '...';
-- ใครเซฟทีหลังก็ทับได้เสมอ ไม่มีใครรู้ว่าเพิ่งเขียนทับงานคนอื่นไป
-- ✅ มี token: xmin ตอนอ่านเข้าไปอยู่ใน WHERE ด้วย
UPDATE "Orders" SET "Status" = 1 WHERE "Id" = '...' AND xmin = 100;
-- ถ้ามีคนแก้ไปก่อน xmin ไม่ใช่ 100 แล้ว → จับได้ 0 แถว → EF Core โยน DbUpdateConcurrencyException

สังเกต​ว่า​เรา​ตั้ง token ไว้​บน ราก Order เท่านั้น ไม่​ได้​ตั้ง​บน OrderLine แต่ละ​บรรทัด — นี่​ไม่ใช่​ความ​บังเอิญ แต่​เป็น​ผล​โดยตรง​จาก​นิยาม​ของ AggregateAggregateกลุ่ม​ของ Entity และ Value Object ที่​ถือ​เป็น​หนึ่ง​หน่วย​ความ​สอดคล้อง มี root เดียว​เป็น​ประตู​เข้า​และ​เป็น​หน่วย​ของ transaction เช่น Order ที่​คุม OrderLine — งาน​ของ​บท​นี้​คือ map Aggregate ทั้ง​ก้อน​ลง SQL โดย​ไม่​ทำลาย invariant ที่ root รักษา​ไว้Tactical Design: ราก aggregate เป็น​ทั้ง​ประตู​เข้า​เดียว เป็น​หน่วย​ของ​ความ​สอดคล้อง และ​เป็น​หน่วย​ของ​ธุรกรรม ดังนั้น​มัน​จึง​เป็นหน่วย​ของ concurrency ด้วย token ตัว​เดียว​บน​ราก​คุ้มครอง​ทั้ง graph เพราะ​การ​เปลี่ยนแปลง​ที่​มี​ความหมาย​ทุก​อย่าง​ต้อง​ผ่าน​ราก และ​ราก​จะ​แก้ column ของ​ตัวเอง (Status, Total) เสมอ — เช่น เพิ่ม​บรรทัด​หนึ่ง​บรรทัด Total ก็​ต้อง​คำนวณ​ใหม่ ทำให้​แถว Orders ถูก UPDATE และ xmin ถูก​ตรวจ​ทุก​ครั้ง

นี่​ยัง​สอด​รับ​กับ​หลัก “1 aggregate ต่อ​หนึ่ง SaveChanges” ที่​จะ​เป็น​แกน​ของ Transaction BoundaryTransaction Boundaryขอบเขต​ที่​กำหนด​ว่า​งาน​ชิ้น​ไหน​ต้อง​สำเร็จ​หรือ​ล้มเหลว​พร้อม​กัน​ทั้งหมด — ปกติ​คือ1 Aggregate ต่อ1 SaveChanges เดียว ข้าม Aggregate ให้​ใช้ eventual consistency แทน​การ​ยัด​หลาย aggregate ไว้​ใน transaction เดียวกันArchitecture ใน​บท​ถัด​ไป — เมื่อ DbContextDbContextจุด​เข้า​หลัก​ของ EF Core ที่​รวม connection, change tracker และ unit of work ไว้​ใน​ตัว​เดียว มี DbSet<T> ต่อ Aggregate หนึ่ง​ตัว — อายุ​สั้น (scoped ต่อ request) ไม่ใช่ singleton ที่​ใช้​ข้าม requestArchitecture เป็น Unit of WorkUnit of Workpattern ที่​รวบรวม​การ​เปลี่ยนแปลง​หลาย object ไว้​เป็น​ชุด​เดียว​แล้ว commit พร้อม​กัน​เป็น transaction เดียว — DbContext ของ EF Core เป็น unit of work อยู่​ใน​ตัว​แล้ว ไม่​ต้อง​เขียน class ห่อ​ซ้ำArchitecture ที่ commit ทั้ง​ชุด​ที​เดียว การ​ตรวจ token ก็​เกิด​ที่​จุด commit จุด​เดียว​นั้น​พอดี ลอง​ดู​สอง​ธุรกรรม​ที่​กด​ยืนยัน​ออเดอร์​ใบ​เดียวกัน​พร้อม​กัน:

sequenceDiagram
  participant A as ธุรกรรม A (พนักงาน ก)
  participant DB as PostgreSQL · แถว Order เลขที่ 42
  participant B as ธุรกรรม B (พนักงาน ข)
  Note over DB: เริ่มต้น xmin = 100
  A->>DB: อ่าน Order 42 (จำ xmin = 100)
  B->>DB: อ่าน Order 42 (จำ xmin = 100)
  Note over A: order.Confirm() แก้ผ่านราก
  Note over B: order.Confirm() แก้ผ่านราก (คนละสำเนา แต่ Order ใบเดียวกัน)
  A->>DB: SaveChanges → UPDATE ... WHERE Id=42 AND xmin=100
  DB-->>A: จับได้ 1 แถว → commit สำเร็จ (xmin เลื่อนเป็น 101)
  B->>DB: SaveChanges → UPDATE ... WHERE Id=42 AND xmin=100
  DB-->>B: จับได้ 0 แถว (ตอนนี้ xmin=101 แล้ว) → ชน
  Note over B: EF Core โยน DbUpdateConcurrencyException<br/>งานของ ข ยังไม่ถูกบันทึก

คำ​บรรยาย​ภาพ: ทั้ง​สอง​ธุรกรรม​อ่าน Order 42 มา​ตอน xmin เท่ากับ 100 เหมือน​กัน ธุรกรรม A เซฟ​ก่อน UPDATE ที่​มี​เงื่อนไข xmin = 100 จึง​จับ​แถว​ได้​และ commit สำเร็จ — พร้อม​กัน​นั้น Postgres เลื่อน xmin ของ​แถว​เป็น 101 พอ​ธุรกรรม B เซฟ​ตาม UPDATE ที่​ยัง​ถือ​เงื่อนไข xmin = 100 จับ​แถว​ได้ 0 เพราะ version เปลี่ยน​ไป​แล้ว EF Core จึง​โยน DbUpdateConcurrencyException แทนที่​จะ​เขียน​ทับ​งาน​ของ A เงียบๆ งาน​ของ B ยังไม่ถูก​บันทึก และ B รู้ตัว

DbUpdateConcurrencyException มี​ความหมาย​เดียว​และ​ชัดเจน: แถว​ที่​เรา​กำลัง​จะ​เขียน ถูก​ธุรกรรม​อื่น​แก้ (หรือ​ลบ) ไป​แล้ว​หลัง​จาก​ที่​เรา​อ่าน​มันSaveChanges คาด​ว่า​จะ​กระทบ 1 แถว แต่​กระทบ​จริง 0 แถว​เพราะ concurrency token ไม่​ตรง ผล​ที่​สำคัญ​ที่สุด​คือ การ​เปลี่ยนแปลง​ของ​เรา​ยัง​ไม่​ถูก​บันทึก ฐาน​ข้อมูล​ยัง​คง​ค่า​ของ​ธุรกรรม​ที่​ชนะ​ไว้​ครบ ไม่มี​ข้อมูล​ปน​กันครึ่งๆ กลางๆ

จุด​ที่​ต้อง​ระวัง​เชิง​สถาปัตยกรรม: DbUpdateConcurrencyException เป็น​ชนิด​ของ EF Core ถ้า​เรา​ปล่อย​ให้​มัน​ทะลุ​ขึ้น​ไป​ถึง​ชั้น Application ตรงๆ แปล​ว่า EF Core รั่ว​ออก​นอก Infrastructure — ผิด​หลัก Persistence Ignorance ที่​คอร์ส​นี้​รักษา​มา​ตลอด ทาง​ที่​สะอาด​คือ​ให้ repository ใน Infrastructure จับ​แล้ว​แปลมัน​เป็น exception ภาษา domain ก่อน​ปล่อย​ออก​ไป:

EfOrderRepository: จับ conflict แล้ว​แปล​เป็น​ภาษา domain
// FoodOrdering.Infrastructure/Persistence/EfOrderRepository.cs
public sealed class EfOrderRepository : IOrderRepository
{
private readonly FoodOrderingDbContext _db;
public EfOrderRepository(FoodOrderingDbContext db) => _db = db;
public Task<Order?> FindAsync(OrderId id, CancellationToken ct) =>
_db.Orders.FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task SaveAsync(Order order, CancellationToken ct)
{
try
{
await _db.SaveChangesAsync(ct); // UPDATE ... WHERE Id = @id AND xmin = @xmin
}
catch (DbUpdateConcurrencyException ex)
{
// แปล exception ของ EF Core เป็นภาษา domain ก่อนปล่อยออกจาก Infrastructure
// (OrderModifiedConcurrentlyException ประกาศไว้ในชั้น Application) — ชั้นบนจะได้
// ไม่ต้องรู้จัก EF Core เลย รักษา Persistence Ignorance ไว้
throw new OrderModifiedConcurrentlyException(order.Id, ex);
}
}
}

จับ conflict ได้​แล้ว​ต้องตัดสิน​ใจ ห้าม​กลืน exception ทิ้ง​เฉยๆ เพราะ​นั่น​เท่ากับ​ยอม​ให้​งาน​หาย​ไป​โดย​ไม่มี​ใคร​รู้ มี​สอง​ทาง​หลัก:

  1. Reload + retry — เหมาะ​เมื่อ​การ​เปลี่ยนแปลง​ของ​เรา​กับ​ของ​คน​ที่​ชนะ​ไม่​ได้​ขัด​กันตรงๆ (เช่น​ต่าง​คน​ต่าง​เพิ่ม​คนละ​บรรทัด) ดึง​ค่า​ล่าสุด​จาก​ฐาน​ข้อมูล​มา​ทับ​สำเนา​เก่า แล้วลอง​ใหม่​บน​ฐาน​ที่​สด:
ทาง​เลือก A — reload ค่า​ล่าสุด​แล้ว​ลอง​ใหม่
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single(); // entry ของ Order ที่ชน
await entry.ReloadAsync(ct); // ดึงค่าปัจจุบัน + xmin ใหม่จาก DB มาทับ
// ...นำการเปลี่ยนแปลงที่ตั้งใจไว้มา apply บนค่าที่สดแล้ว แล้ว SaveChangesAsync อีกครั้ง...
}
  1. Surface ให้​ผู้​ใช้​ตัดสิน — เหมาะ​เมื่อ​ทั้ง​สอง​ฝ่าย​แก้ field เดียวกัน​จน​ไม่มี​ทาง merge อัตโนมัติ​แบบ​ปลอดภัย ปล่อย OrderModifiedConcurrentlyException ที่​แปล​ไว้​ให้​ทะลุ​ขึ้น​ไป​ถึง​ชั้น API แล้ว​ตอบ 409 Conflict พร้อม​ข้อความ​ว่า “ออเดอร์​นี้​เพิ่ง​ถูก​แก้​ไป โปรด​โหลด​ใหม่​แล้ว​ลอง​อีก​ครั้ง” ให้​มนุษย์​เป็น​คน​เลือก​ว่า​จะ​เอา version ไหน
flowchart TD
  S["SaveChanges โยน<br/>DbUpdateConcurrencyException"] --> Q{"merge อัตโนมัติ<br/>ได้อย่างปลอดภัยไหม?"}
  Q -- "ได้ (แก้คนละส่วน ไม่ขัดกัน)" --> R["reload ค่าล่าสุดจาก DB<br/>แล้วลองใหม่บนฐานที่สด"]
  Q -- "ไม่ได้ (แก้ field เดียวกัน)" --> U["ตอบ 409 Conflict<br/>ให้ผู้ใช้ตัดสินใจเอง"]
  R --> D["บันทึกสำเร็จ ไม่มีงานของใครหาย"]
  U --> D

คำ​บรรยาย​ภาพ: ทั้ง​สอง​ทาง​จบ​ที่​จุด​เดียวกัน — ไม่มี​การ​เขียน​ทับ​เงียบๆ ความ​ต่าง​อยู่​ที่​ว่า​ใคร​เป็น​คน​ตัดสิน​ใจ merge: code (เมื่อ​ปลอดภัย​พอ) หรือ​มนุษย์ (เมื่อ​การ​ชน​กัน​ต้อง​ใช้​วิจารณญาณ)

ทำไม​ต้อง​ลงทุน​กับ​เรื่อง​นี้? เพราะ​ทาง​เลือก​ตรง​ข้าม — last-write-wins หรือ “คน​เซฟ​ทีหลัง​ชนะ” — คือ​ค่า default เมื่อไม่มี concurrency token และ​มัน​อันตราย​เงียบๆ ลอง​นึก​ภาพออเดอร์​ใบ​หนึ่ง​ราคา​รวม 250 บาท พนักงาน​สอง​คน​เปิด​พร้อม​กัน:

  • พนักงาน ก เพิ่ม​ผัด​ไทย​หนึ่ง​จาน Total ใน​สำเนา​ของ​เขา​กลาย​เป็น 310 บาท แล้ว​เซฟ
  • พนักงาน ข ทำงาน​จาก​สำเนา​เก่า​ที่ Total ยัง 250 เพิ่ม​ชา​ไทย​หนึ่ง​แก้ว Total ของ​เขา​กลาย​เป็น 280 บาท แล้ว​เซฟ​ตาม

ถ้า​ไม่มี token คำ​สั่ง UPDATE ของ ข ไม่มี​เงื่อนไข version มัน​จึง​เขียน Total = 280 ทับ​สำเร็จ — ผัด​ไทย 60 บาท​ของ ก หาย​ไป​ทั้ง​ที่​ลูกค้า​สั่ง​จริง ไม่มี error ไม่มี log ไม่มี​ใคร​รู้ ครัว​ไม่​เห็น​ผัด​ไทย ยอด​เงิน​ไม่​ตรง​กับ​ของ​ที่​ส่ง นี่​คือ lost update คลาสสิก: การ​แก้​ที่​คำนวณ​จาก​ค่าที่​ล้าสมัย​ไป​แล้ว ลบ​ผล​ของ​อีก​คน​ทิ้ง​อย่าง​เงียบเชียบ

จุด​ที่​เงิน​เข้า​มา​เกี่ยว​คือ​จุด​ที่​เดิมพัน​สูง​ที่สุด — ยอด​ออเดอร์​ผิด การ​ตัด​เงิน​ซ้ำ ยอด​คง​เหลือ​ที่​บวก​ลบ​ไม่​ตรง ล้วน​เริ่ม​จาก lost update แบบ​นี้​ทั้งนั้น concurrency token เปลี่ยน “หาย​เงียบๆ” ให้​กลาย​เป็น “conflict ที่​มอง​เห็น​และ​จัดการ​ได้” ซึ่ง​เป็น​ก้าว​แรก​ของ​ความ​ถูกต้อง​เรื่อง​เงิน เรื่อง​นี้​จริงจัง​พอที่ คอร์ส​ที่ 10 — Payments & Ledger จะ​ยก​ทั้ง​คอร์สมา​ว่าด้วย​การ​ทำให้​เงิน​ไม่มี​วัน​หาย​หรือ​เกิด​เอง​จาก​อากาศ

🔗 อ้างอิง​เพิ่มเติม​ใน DevIQ

เจาะ​ลึก​แนวคิด​ใน​บท​นี้​ต่อ​ได้ที่​คลัง​อ้างอิง DevIQ:

  • Aggregate — ราก aggregate เป็น​หน่วย​ของ​ความ​สอดคล้อง​และ​ของ transaction จึง​เป็น​ที่​ที่​เรา​วาง concurrency token ไว้ตัว​เดียว​เพื่อ​คุ้มครอง​ทั้ง graph
  • Unit of WorkDbContext เป็น unit of work ใน​ตัว จุด SaveChanges เดียว​คือ​ที่​ที่ token ถูก​ตรวจ​และ conflict ถูก​จับ
  • Payments & Ledger (คอร์ส​ที่ 10) — ที่​ที่​ปัญหา lost update เรื่อง​เงิน​ถูก​ยก​มา​ว่า​อย่าง​จริงจัง​เต็ม​คอร์ส

เช็กความเข้าใจ — บทที่ 5

ข้อ 1 / 3

concurrency token ทำหน้าที่อะไร?