งานบางอย่างไม่ต้องเสร็จก่อนตอบผู้ใช้
ลองนึกถึงตอนลูกค้ากดสั่งซื้อในเว็บขายของออนไลน์ หลังบันทึกคำสั่งซื้อแล้ว ระบบยังต้องส่งอีเมลยืนยัน แจ้ง LINE ให้ร้าน สร้างใบเสร็จเป็น PDF และส่งข้อมูลไปให้ขนส่ง ถ้าทำทุกอย่างใน request เดียว ลูกค้าต้องนั่งรอหน้าจอหมุนจนครบทุกงาน และถ้า SMTP หรือ API ของขนส่งช้าสักตัว หน้าสั่งซื้อก็ช้าตาม หรือ error ทั้งที่คำสั่งซื้อบันทึกไปแล้ว
Queue แยกงานพวกนี้ออกไปทำเบื้องหลัง หน้าเว็บแค่จดว่ามีงานอะไรต้องทำ แล้วตอบลูกค้าทันที งานที่เหมาะกับคิวคืองานที่ช้า งานที่พึ่งระบบภายนอก และงานที่ลองใหม่ได้ถ้าพัง

หน้าเว็บเก็บงานลงคิวแล้วตอบทันที worker ที่รันอยู่อีกฝั่งค่อยหยิบงานไปทำ
Job คืองานหนึ่งชิ้นที่เก็บลงคิวได้
ใน Laravel งานแต่ละอย่างเขียนเป็นคลาส job สร้างด้วย php artisan make:job ใน handle() ใส่สิ่งที่ต้องทำ แล้วสั่งเข้าคิวจาก controller ด้วย dispatch()
class SendOrderConfirmation implements ShouldQueue
{
use Queueable;
public function __construct(public Order $order) {}
public function handle(): void
{
Mail::to($this->order->email)->send(new OrderConfirmed($this->order));
}
}
// ใน controller หลังบันทึกคำสั่งซื้อ
SendOrderConfirmation::dispatch($order);
ตอน dispatch Laravel แปลง job เป็นข้อความเก็บลงคิว model อย่าง $order เก็บแค่ id ไว้ แล้วโหลดใหม่จากฐานข้อมูลตอน worker หยิบไปทำ ถ้า dispatch ระหว่าง database transaction ให้ job รอ transaction commit ก่อน ไม่อย่างนั้น worker อาจหยิบงานไปทำก่อนข้อมูลจะบันทึกจริง
Connection บอกว่าคิวเก็บไว้ที่ไหน
QUEUE_CONNECTION ใน .env เลือกที่เก็บคิว โปรเจกต์ Laravel ใหม่ตั้งไว้เป็น database ซึ่งเก็บงานในตาราง jobs ของฐานข้อมูลที่ใช้อยู่แล้ว ไม่ต้องลงอะไรเพิ่ม
| connection | เก็บงานไว้ที่ | เหมาะกับ |
|---|---|---|
sync |
ไม่เข้าคิว รันทันทีใน request | ตอนเขียนโค้ดหรือรัน test |
database |
ตาราง jobs |
งานไม่เยอะ ไม่อยากเพิ่ม service |
redis |
Redis | งานเยอะ อยากได้ dashboard ของ Horizon |
ใน connection เดียวกันยังแบ่งคิวย่อยตามชื่อได้ เช่น ให้งานชำระเงินอยู่คิว payment แล้วสั่ง worker ให้ทำคิวนี้ก่อน (--queue=payment,default) งานด่วนจะได้ไม่ต้องรอหลังงานส่งอีเมลเป็นร้อยฉบับ
Worker คือ process ที่รันค้างไว้เพื่อหยิบงาน
ถ้าไม่มี worker งานจะกองอยู่ในคิวเฉย ๆ worker เริ่มด้วย php artisan queue:work แล้วรันค้างไว้ หยิบงานทีละชิ้นมาทำ พอคิวว่างก็รอสักครู่แล้วเช็กใหม่
บน VPS เราให้ Supervisor หรือ systemd คุม worker ไว้ ถ้า process ตายก็เปิดใหม่ให้เอง บน shared hosting ที่รัน process ค้างไม่ได้ ทางที่ใช้กันมาตลอดคือตั้ง cron ทุกนาทีให้รัน queue:work --stop-when-empty ทำงานที่ค้างจนหมดแล้วออก งานจะช้ากว่าบ้างแต่ใช้ได้จริง
worker โหลดโค้ดเข้า memory ครั้งเดียวตอนเริ่ม แก้โค้ดแล้ว worker ไม่เห็นจนกว่าจะเริ่มใหม่ ทุกครั้งที่ deploy จึงต้องสั่ง php artisan queue:restart ให้ worker จบงานที่ทำอยู่แล้วออก ให้ Supervisor เปิดตัวใหม่ที่โหลดโค้ดใหม่ขึ้นมา
timeout, tries และ retry_after คุมงานที่ช้าหรือพัง
สามค่านี้ตั้งผิดแล้วอาการจะงงที่สุด เช่น งานรันซ้ำ หรือขึ้นว่าล้มเหลวทั้งที่ทำเสร็จแล้ว
timeoutคือเวลาที่ job หนึ่งรันได้ ถ้าเกิน worker จะฆ่า process ทิ้ง ค่าเริ่มต้นของqueue:workคือ 60 วินาที ตั้งต่อ job ได้ด้วยpublic $timeouttriesคือจำนวนครั้งที่ลองได้ ค่าเริ่มต้นคือ 1 ครั้ง ถ้าเกินแล้วยังพัง งานจะไปอยู่ในตารางfailed_jobsดูได้ด้วยqueue:failedและสั่งลองใหม่ด้วยqueue:retryใส่backoffไว้ให้รอสักพักก่อนลองครั้งถัดไปretry_afterอยู่ในconfig/queue.phpของแต่ละ connection (ค่าเริ่มต้น 90 วินาที) คือเวลาที่คิวยอมให้ worker ถืองานไว้ ถ้าเกินแล้วงานยังไม่เสร็จ คิวจะถือว่า worker ตายไปแล้ว และปล่อยงานกลับให้ worker ตัวอื่นหยิบ
กฎที่ docs ของ Laravel ย้ำไว้คือ timeout ต้องสั้นกว่า retry_after เสมอ ถ้า job รันนานกว่า retry_after แต่ยังไม่ถึง timeout คิวจะปล่อยงานเดียวกันให้ worker อีกตัวทำซ้อนกันทั้งที่ตัวแรกยังทำอยู่ ลูกค้าก็ได้อีเมลสองฉบับ
แม้ตั้งค่าถูกแล้ว งานก็ยังรันซ้ำได้ในบางกรณี เช่น worker ตายหลังส่งอีเมลไปแล้วแต่ก่อนบอกคิวว่าเสร็จ job ที่ส่งของออกไปข้างนอกจึงควรทำให้ส่งซ้ำได้โดยไม่เกิดผลซ้ำ
Horizon ช่วยเมื่องานในคิวเยอะขึ้น
พอใช้ Redis เป็นคิวแล้ว Laravel Horizon ช่วยคุม worker ให้ ตั้งจำนวน worker ต่อคิวไว้ใน config ไฟล์เดียว และมี dashboard ให้เห็นว่างานค้างกี่ชิ้น ใช้เวลาเท่าไหร่ และพังเพราะอะไร ส่วนวิธีตั้ง timeout ให้เป็นชั้น และเช็กก่อนเพิ่ม worker ดูต่อได้ในวิธีจูน Laravel Horizonครับ






