การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์
ประสานบริการผ่านเหตุการณ์และการทำงานซ้ำโดยให้ผลลัพธ์เดิม
การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์ เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- เหตุผลที่ควรใช้การส่งข้อความแบบอะซิงโครนัส
- การทำงานกับ RabbitMQ ใน PHP
- Apache Kafka กับ PHP
- การสร้างเวิร์กโฟลว์ที่ขับเคลื่อนด้วยเหตุการณ์