0Pricing
PHP Academy · บทเรียน

การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์

ประสานบริการผ่านเหตุการณ์และการทำงานซ้ำโดยให้ผลลัพธ์เดิม

การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์ เป็นบทเรียน PHP Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน PHP Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน

กระบวนการทำงานที่ขับเคลื่อนด้วยเหตุการณ์

ข้อความเดียวจัดการได้ง่าย แต่กระบวนการทำงานอย่าง “วางคำสั่งซื้อ → สำรองสินค้า → เรียกเก็บเงินจากบัตร → จัดส่ง → แจ้งเตือน” ซึ่งครอบคลุมหลายบริการ คือจุดที่การออกแบบที่ขับเคลื่อนด้วยเหตุการณ์แสดงประโยชน์ได้อย่างเต็มที่ และเป็นจุดที่อาจสร้างปัญหาได้หากทำอย่างไม่รอบคอบ

บทเรียนนี้ครอบคลุมการประสานงานแบบกระจายศูนย์เทียบกับแบบมีตัวควบคุมกลาง รูปแบบเอาต์บ็อกซ์สำหรับการเผยแพร่แบบอะตอมิก ซากาสำหรับการย้อนกลับแบบกระจาย และคุณสมบัติการทำซ้ำที่ปลอดภัยซึ่งช่วยยึดทุกส่วนเข้าด้วยกัน

การประสานงานแบบกระจายศูนย์เทียบกับแบบมีตัวควบคุมกลาง

มีสองวิธีในการประสานกระบวนการที่มีหลายขั้นตอน:

  • การประสานงานแบบกระจายศูนย์ — แต่ละบริการตอบสนองต่อเหตุการณ์และส่งเหตุการณ์ของตนเองออกมา โดยไม่มีศูนย์กลางควบคุม ระบบเชื่อมโยงกันอย่างหลวม ๆ แต่ลำดับการทำงานโดยรวมไม่ชัดเจนและติดตามได้ยาก
  • การประสานงานแบบมีตัวควบคุมกลาง — ตัวประสานงานส่วนกลางจะบอกแต่ละบริการว่าต้องทำอะไรต่อไป ลำดับชัดเจนและสังเกตการณ์ได้ แต่ตัวประสานงานจะเป็นจุดที่ทำให้เกิดการเชื่อมโยงกัน

หลักทั่วไปคือ ใช้การประสานงานแบบกระจายศูนย์สำหรับการกระจายงานแบบง่าย และใช้การประสานงานแบบมีตัวควบคุมกลางเมื่อกระบวนการมีหลายขั้นตอนตามลำดับและต้องการสถานะที่มองเห็นได้

เหตุการณ์เทียบกับคำสั่ง

ตั้งชื่อข้อความของคุณอย่างตั้งใจ:

  • เหตุการณ์ระบุข้อเท็จจริงที่เกิดขึ้นในอดีต: OrderPlaced ผู้เผยแพร่ไม่จำเป็นต้องสนใจว่าใครกำลังรับฟังอยู่
  • คำสั่งร้องขอการดำเนินการในอนาคตจากตัวจัดการที่ระบุ: ChargeCard

เหตุการณ์ขับเคลื่อนการประสานงานแบบกระจายศูนย์ ส่วนคำสั่งขับเคลื่อนการประสานงานแบบมีตัวควบคุมกลาง การใช้คำศัพท์ปะปนกัน (เหตุการณ์ที่แอบคาดหวังให้มีตัวจัดการเพียงรายเดียว) เป็นสาเหตุทั่วไปของการเชื่อมโยงที่ซ่อนอยู่

<?php
final class OrderPlaced {
    public function __construct(
        public readonly string $orderId,
        public readonly string $customerId,
        public readonly int $amountCents,
        public readonly string $occurredAt,
    ) {}
}

$e = new OrderPlaced('o-42', 'c-7', 1990, gmdate('c'));
echo json_encode($e), "\n";

ปัญหาการเขียนสองระบบ

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

<?php
// BROKEN: not atomic. A crash between the two lines corrupts state.
function placeOrder(PDO $db, $broker, array $o): void {
    $db->prepare('INSERT INTO orders ...')->execute($o);
    // <-- crash here = row exists but no event ever published
    $broker->publish('OrderPlaced', json_encode($o));
}

เอาต์บ็อกซ์แบบธุรกรรม

วิธีแก้คือรูปแบบเอาต์บ็อกซ์: ภายในธุรกรรมฐานข้อมูลเดียวกับที่เปลี่ยนแปลงข้อมูล ให้แทรกเหตุการณ์ลงในตาราง outbox กระบวนการส่งต่อแยกต่างหากจะอ่านแถวที่ยังไม่ได้เผยแพร่และส่งไปยังโบรกเกอร์ การคอมมิตแบบอะตอมิกเพียงครั้งเดียวช่วยขจัดการเขียนสองระบบ

<?php
function placeOrder(PDO $db, array $o): void {
    $db->beginTransaction();
    $db->prepare('INSERT INTO orders (id, total) VALUES (?, ?)')
       ->execute([$o['id'], $o['total']]);
    // Same transaction -> atomic with the business write
    $db->prepare('INSERT INTO outbox (id, type, payload) VALUES (?, ?, ?)')
       ->execute([bin2hex(random_bytes(8)), 'OrderPlaced', json_encode($o)]);
    $db->commit();
}

ตัวส่งต่อ (ผู้เผยแพร่)

ผู้ปฏิบัติงานจะตรวจสอบเอาต์บ็อกซ์เป็นระยะ (หรือติดตามบันทึกการเปลี่ยนแปลงฐานข้อมูลผ่าน CDC) เผยแพร่แต่ละแถว แล้วทำเครื่องหมายว่าได้ส่งแล้ว เนื่องจากตัวส่งต่ออาจหยุดทำงานหลังเผยแพร่แต่ก่อนทำเครื่องหมาย ตัวส่งต่อจึงมีการส่งมอบแบบอย่างน้อยหนึ่งครั้งเช่นกัน ซึ่งไม่เป็นปัญหา เพราะผู้รับข้อความสามารถจัดการข้อความซ้ำได้อย่างปลอดภัย

<?php
function relayOutbox(PDO $db, $broker): void {
    $rows = $db->query(
        'SELECT id, type, payload FROM outbox
         WHERE published_at IS NULL ORDER BY created_at LIMIT 100'
    )->fetchAll(PDO::FETCH_ASSOC);

    foreach ($rows as $r) {
        $broker->publish($r['type'], $r['payload'], messageId: $r['id']);
        $db->prepare('UPDATE outbox SET published_at = now() WHERE id = ?')
           ->execute([$r['id']]);
    }
}

ผู้รับข้อความที่ทำซ้ำได้อย่างปลอดภัย (อีกครั้ง)

เนื่องจากทั้งตัวส่งต่อและโบรกเกอร์มีการส่งมอบแบบอย่างน้อยหนึ่งครั้ง ตัวจัดการปลายทางจะเห็นข้อความซ้ำ ผู้รับข้อความแต่ละรายจะบันทึกรหัสข้อความที่ประมวลผลแล้ว และหยุดการทำงานทันทีเมื่อพบข้อความซ้ำ — กลไกกรองข้อความซ้ำแบบเดียวกับที่เรียนมาก่อนหน้านี้ ซึ่งตอนนี้นำมาใช้แยกตามบริการ

<?php
function onOrderPlaced(PDO $db, string $messageId, array $data): void {
    $db->beginTransaction();
    try {
        $db->prepare('INSERT INTO inbox (message_id) VALUES (?)')
           ->execute([$messageId]); // unique index = dedup
    } catch (PDOException $e) {
        $db->rollBack();
        return; // already handled this message
    }
    reserveStock($data['orderId']);
    $db->commit();
}
function reserveStock(string $id): void {}

ซากา: การย้อนกลับแบบกระจาย

คุณไม่สามารถเปิดธุรกรรม ACID เดียวครอบคลุมหลายบริการได้ ซากาจึงจำลองกระบวนการที่ทำงานเป็นเวลานานให้เป็นลำดับของธุรกรรมเฉพาะบริการ โดยแต่ละธุรกรรมมีการดำเนินการชดเชยที่ย้อนผลของธุรกรรมนั้น หากขั้นตอนที่ 3 ล้มเหลว ให้ดำเนินการชดเชยของขั้นตอนที่ 2 และ 1 ย้อนกลับตามลำดับ

ตัวอย่างเช่น การชำระเงินล้มเหลวหลังจากสำรองสินค้าแล้ว → ส่ง ReleaseStock เพื่อชดเชย ไม่มีการย้อนกลับโดยอัตโนมัติ — คุณต้องออกแบบการยกเลิกผลสำหรับทุกขั้นตอน

ซากาที่มีตัวควบคุมกลาง

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

<?php
function handleStepResult(array $saga, string $step, bool $ok, $bus): array {
    if ($ok) {
        $next = ['reserveStock' => 'chargeCard', 'chargeCard' => 'ship'][$step] ?? null;
        if ($next) { $bus->send($next, $saga['orderId']); $saga['state'] = $next; }
        else { $saga['state'] = 'completed'; }
    } else {
        // Run compensations in reverse for whatever already succeeded
        foreach (array_reverse($saga['done']) as $s) {
            $bus->send('compensate.' . $s, $saga['orderId']);
        }
        $saga['state'] = 'compensating';
    }
    return $saga;
}

การหมดเวลาในกระบวนการทำงานระยะยาว

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

<?php
function reapTimedOutSagas(PDO $db, $bus): void {
    $rows = $db->query(
        "SELECT order_id, state FROM sagas
         WHERE state NOT IN ('completed','compensating')
           AND deadline_at < now()"
    )->fetchAll(PDO::FETCH_ASSOC);

    foreach ($rows as $r) {
        echo "Saga {$r['order_id']} timed out at step {$r['state']}\n";
        $bus->send('saga.compensate', $r['order_id']); // trigger rollback
    }
}

การกำหนดเวอร์ชันและการสังเกตการณ์ระบบ

เวิร์กโฟลว์มีอายุการใช้งานหลายปี เหตุการณ์จึงต้องเปลี่ยนแปลงได้อย่างปลอดภัย:

  • เพิ่ม version (หรือสคีมา) ให้กับทุกเหตุการณ์ ผู้รับเหตุการณ์ต้องรองรับฟิลด์ใหม่ที่ไม่รู้จัก และไม่ควรถือว่าฟิลด์นั้นมีอยู่เสมอ
  • ควรเลือกการเปลี่ยนแปลงแบบเพิ่มเท่านั้น และห้ามนำความหมายของฟิลด์เดิมไปใช้ใหม่
  • ส่งต่อ รหัสสหสัมพันธ์ผ่านทุกข้อความ เพื่อให้ติดตามธุรกรรมทางธุรกิจรายการเดียวข้ามบริการทั้งหมดได้จากบันทึกและการติดตามระบบ

หากไม่มีรหัสสหสัมพันธ์ การแก้ไขข้อบกพร่องของลำดับงานที่ประสานกันผ่านห้าบริการแทบจะเป็นไปไม่ได้

<?php
$envelope = [
    'type'          => 'OrderPlaced',
    'version'       => 2,
    'correlationId' => $incoming['correlationId'] ?? bin2hex(random_bytes(8)),
    'occurredAt'    => gmdate('c'),
    'data'          => ['orderId' => 'o-42'],
];
echo json_encode($envelope, JSON_PRETTY_PRINT), "\n";

ตรวจสอบอย่างรวดเร็ว

การหลีกเลี่ยงปัญหาการเขียนข้อมูลสองแหล่ง

สรุปทบทวน

ขณะนี้คุณสามารถออกแบบเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์ให้เชื่อถือได้แล้ว:

  • เลือก การประสานงานแบบกระจาย (เหตุการณ์) หรือ การประสานงานแบบมีตัวควบคุม (คำสั่ง) ตามความซับซ้อนของลำดับงาน
  • แก้ปัญหาการเขียนข้อมูลสองแหล่งด้วย เอาต์บ็อกซ์เชิงธุรกรรมและตัวส่งต่อ
  • ทำให้ผู้รับเหตุการณ์ทุกราย ทำซ้ำได้โดยไม่เปลี่ยนผลลัพธ์ ด้วยคีย์กล่องรับ/ขจัดข้อมูลซ้ำ
  • ใช้ กระบวนการซาการ่วมกับการดำเนินการชดเชยเพื่อย้อนกลับการเปลี่ยนแปลงในระบบแบบกระจาย
  • กำหนดเวอร์ชันให้เหตุการณ์ด้วยการเพิ่มฟิลด์เท่านั้น และส่งต่อ รหัสสหสัมพันธ์เพื่อให้ตรวจสอบย้อนกลับได้

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

คำถามที่พบบ่อย

บทเรียน “การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส PHP Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์”

ประสานบริการผ่านเหตุการณ์และการทำงานซ้ำโดยให้ผลลัพธ์เดิม คุณปฏิบัติ PHP Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน PHP Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน PHP Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน PHP Academy นี้ได้ไหม

ได้ บทเรียน PHP Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. เหตุผลที่ควรใช้การส่งข้อความแบบอะซิงโครนัส
  2. การทำงานกับ RabbitMQ ใน PHP
  3. Apache Kafka กับ PHP
  4. การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์
← กลับไปที่ PHP Academy