ปัญหา: งานตามกำหนดเวลาโดยไม่เพิ่มโครงสร้างพื้นฐาน\n\nแอปส่วนใหญ่ต้องให้มีงานที่เกิดขึ้นภายหลังหรือเป็นตามตารางเวลา: ส่งอีเมลติดตาม, ตรวจสอบบิลรายคืน, ลบระเบียนเก่า, สร้างรายงานใหม่, หรือรีเฟรชแคช\n\nตอนแรกมักอยากใส่ระบบคิวเต็มรูปแบบเพราะรู้สึกว่าเป็นทางที่ “ถูกต้อง” สำหรับงานแบ็กกราวด์ แต่คิวเพิ่มชิ้นส่วนที่ต้องดูแล: บริการอีกตัวให้รัน, ต้องมอนิเตอร์, ดีพลอย, และดีบัก สำหรับทีมเล็ก (หรือผู้ก่อตั้งคนเดียว) ภาระที่เพิ่มมานั้นอาจทำให้ความเร็วในการพัฒนาช้าลง\n\nคำถามจริง ๆ คือ: จะรันงานตามกำหนดอย่างน่าเชื่อถือโดยไม่ต้องตั้งโครงสร้างพื้นฐานเพิ่มได้อย่างไร?\n\nความพยายามเริ่มต้นที่พบบ่อยคือเรียบง่าย: เพิ่มรายการ cron ที่เรียก endpoint และ endpoint นั้นทำงาน มันใช้ได้จนกว่าจะใช้ไม่ได้ เมื่อมีมากกว่าหนึ่งเซิร์ฟเวอร์, ดีพลอยช่วงผิดเวลา, หรือหากงานใช้เวลานานกว่าที่คาดไว้ คุณจะเริ่มเห็นความผิดพลาดที่สับสน\n\nงานตามกำหนดมักพังในแบบที่คาดเดาได้ไม่กี่แบบ:\n\n- รันซ้ำ: สองเซิร์ฟเวอร์รันงานเดียวกัน ทำให้ใบแจ้งหนี้ถูกสร้างซ้ำหรืออีเมลถูกส่งสองครั้ง\n- งานหาย: การเรียก cron ล้มเหลวระหว่างดีพลอยและไม่มีใครสังเกตจนผู้ใช้ร้องเรียน\n- ล้มเหลวเงียบ: งาน error ครั้งหนึ่งแล้วไม่รันอีกเพราะไม่มีแผน retry\n- งานทำไม่ครบ: งาน crash กลางทางแล้วทิ้งข้อมูลในสถานะแปลกๆ\n- ไม่มีบันทึก: ไม่สามารถตอบได้ว่า “ครั้งสุดท้ายที่รันเมื่อไหร่?” หรือ “เมื่อคืนเกิดอะไรขึ้น?”\n\nรูปแบบ cron + ฐานข้อมูลเป็นทางเลือกกลาง คุณยังใช้ cron เพื่อ “ปลุก” ตามตาราง แต่คุณเก็บเจตนางานและสถานะงานในฐานข้อมูลเพื่อให้ระบบสามารถประสานงาน, ลองใหม่, และบันทึกสิ่งที่เกิดขึ้น\n\nเหมาะเมื่อคุณมีฐานข้อมูลตัวเดียว (มักเป็น PostgreSQL), ชนิดงานจำนวนไม่มาก, และต้องการพฤติกรรมที่คาดเดาได้พร้อมงานปฏิบัติการน้อยที่สุด มันยังเหมาะกับแอปที่สร้างเร็วบนสแต็กสมัยใหม่ (เช่น React + Go + PostgreSQL)\n\nมันไม่เหมาะเมื่อคุณต้องการ throughput สูงมาก, งานระยะยาวที่ต้องสตรีมความคืบหน้า, การเรียงลำดับเข้มงวดข้ามชนิดงานจำนวนมาก, หรือการแตกพัด (fan-out) หนัก ๆ (หลายพันซับทาสก์ต่อนาที) ในกรณีเหล่านั้น ระบบคิวจริง ๆ และ worker เฉพาะทางมักคุ้มค่า\n\n## แนวคิดหลักแบบเข้าใจง่าย\n\nรูปแบบ cron + ฐานข้อมูลรันงานแบ็กกราวด์ตามตารางโดยไม่ต้องใช้ระบบคิวเต็มรูปแบบ คุณยังใช้ cron (หรือ scheduler อื่น) แต่ cron ไม่ตัดสินใจว่าจะรันอะไร มันแค่ปลุก worker บ่อย ๆ (รอบละนาทีเป็นเรื่องปกติ) ฐานข้อมูลเป็นตัวตัดสินว่างานไหนถึงเวลาและมั่นใจว่า worker เพียงตัวเดียวจะรับงานแต่ละตัว\n\nคิดว่ามันเหมือนบอร์ดเช็คลิสต์ที่หลายคนใช้ร่วมกัน Cron คือคนที่เดินเข้าห้องทุกนาทีและถามว่า “มีใครต้องทำอะไรตอนนี้ไหม?” ฐานข้อมูลคือบอร์ดที่บอกว่างานไหนถึงเวลา งานไหนถูกจับ และงานไหนเสร็จแล้ว\n\nส่วนประกอบไม่ซับซ้อน:\n\n- ทริกเกอร์ scheduler เดียวรันบ่อย\n- ตาราง jobs เก็บ “อะไร” และ “เมื่อไหร่” (เวลาที่ถึงกำหนด) พร้อมสถานะและจำนวนครั้งที่พยายาม\n- หนึ่งหรือมากกว่า worker ดึงตาราง โกงงาน และทำงาน\n- การโกรงานต้องใช้ล็อกในฐานข้อมูลเพื่อไม่ให้สอง worker จับแถวเดียวกัน\n- ฐานข้อมูลยังคงเป็นแหล่งความจริงสำหรับสิ่งที่รัน สิ่งที่ล้มเหลว และสิ่งที่ควรลองใหม่\n\nตัวอย่าง: คุณอยากส่งการเตือนใบแจ้งหนี้ทุกเช้า, รีเฟรชแคชทุก 10 นาที, และล้าง sessions เก่าคืนละหนึ่งครั้ง แทนที่จะมีคำสั่ง cron แยกสามชุด (แต่ละอันมีโหมดซ้อนทับและข้อผิดพลาดต่างกัน) ให้เก็บรายการงานไว้ที่เดียว Cron เริ่ม process worker เดียวกัน Worker ถาม Postgres ว่า “อะไรถึงเวอตอนนี้?” และ Postgres ตอบโดยให้ worker จับงานได้อย่างปลอดภัยทีละงาน\n\nมันปรับขนาดแบบค่อยเป็นค่อยไป คุณเริ่มด้วย worker ตัวเดียวบนเซิร์ฟเวอร์หนึ่ง ภายหลังคุณอาจรันห้า worker ข้ามหลายเซิร์ฟเวอร์ ข้อตกลงยังคงเหมือนเดิม: ตารางคือสัญญา\n\nการเปลี่ยนแนวคิดคือ: cron เป็นเพียงการปลุก ฐานข้อมูลเป็นตำรวจกำกับการจราจรที่ตัดสินว่าสิ่งใดอนุญาตให้รัน บันทึกสิ่งที่เกิดขึ้น และให้ประวัติชัดเจนเมื่อมีปัญหา\n\n## การออกแบบตาราง jobs (สคีมาปฏิบัติ)\n\nรูปแบบนี้ทำงานได้ดีที่สุดเมื่อฐานข้อมูลของคุณกลายเป็นแหล่งความจริงว่าอะไรควรรัน เมื่อไหร่ควรรัน และเกิดอะไรขึ้นครั้งสุดท้าย สคีมานั้นไม่ซับซ้อน แต่รายละเอียดเล็กน้อย (ฟิลด์ล็อกและดัชนีที่ถูกต้อง) จะช่วยได้มากเมื่อโหลดเพิ่มขึ้น\n\n### ตารางเดียวหรือสองตาราง?\n\nสองแนวทางที่พบบ่อย:\n\n- ตารางรวมหนึ่งตาราง เมื่อคุณสนใจเพียงสถานะล่าสุดของแต่ละงาน (เรียบง่าย ไม่ต้อง join เยอะ)\n- สองตาราง ถ้าคุณต้องการแยกระหว่าง “งานนี้คืออะไร” กับ “แต่ละครั้งที่มันถูกรัน” (มีประวัติชัดเจน ง่ายต่อการดีบัก)\n\nถ้าคุณคาดว่าจะดีบักความล้มเหลวบ่อย ให้เก็บประวัติไว้ ถ้าต้องการเซ็ตอัพเล็กที่สุด ให้เริ่มด้วยตารางเดียวแล้วเพิ่มประวัติทีหลัง\n\n### สคีมาปฏิบัติ (เวอร์ชันสองตาราง)\n\nนี่คือลักษณะสำหรับ PostgreSQL ถ้าคุณสร้างใน Go กับ PostgreSQL คอลัมน์เหล่านี้จับคู่กับ struct ได้เรียบร้อย\n\n\n\nรายละเอียดเล็ก ๆ ที่ช่วยได้ในภายหลัง:\n\n- เก็บ เป็นสตริงสั้นที่คุณสามารถ route ได้ (เช่น )\n- เก็บ เป็น เพื่อให้พัฒนาได้โดยไม่ต้อง migration บ่อย\n- คือ “เวลาถัดไปที่ถึงกำหนด” Cron (หรือสคริปต์ scheduler) กำหนดค่า และ worker เป็นผู้บริโภค\n- และ ให้ worker จับงานโดยไม่ชนกัน\n- ควรเป็นข้อความสั้นที่อ่านได้สำหรับมนุษย์ เก็บ stack trace แยกที่อื่นถ้าจำเป็น\n\n### ดัชนีที่ควรมี\n\nหากไม่มีดัชนี worker จะสแกนเยอะเกินไป เริ่มด้วย:\n\n- ดัชนีเพื่อค้นหางานที่ถึงเวรได้เร็ว: \n- ดัชนีช่วยตรวจจับล็อกหมดเวลา: \n- ทางเลือก: partial index สำหรับงานที่ active เท่านั้น (เช่น status ใน และ )\n\nดัชนีเหล่านี้ทำให้การค้นหา “งานถัดไปที่รันได้” รวดเร็วแม้ตารางจะโตขึ้น\n\n## การล็อกและการโกรงานอย่างปลอดภัย\n\nเป้าหมายง่าย ๆ คือ: worker หลายตัวอาจรัน แต่มีเพียงตัวเดียวเท่านั้นที่ควรจับงานเฉพาะ หากสอง worker ประมวลผลแถวเดียวกันคุณจะได้อีเมลซ้ำ, การคิดเงินซ้ำ, หรือข้อมูลสกปรก\n\nวิธีที่ปลอดภัยคือถือว่าการโกรงานเป็นเหมือน “สัญญาเช่า (lease)” Worker ทำเครื่องหมายว่าได้ล็อกงานไว้ช่วงสั้น ๆ หาก worker crash สัญญาจะหมดอายุและ worker ตัวอื่นสามารถรับงานได้ นั่นคือเหตุผลที่มี \n\n### ใช้ lease เพื่อให้การ crash ไม่บล็อกงานตลอดไป\n\nหากไม่มี lease worker อาจล็อกงานแล้วไม่ปลดล็อกเลย (process ถูกฆ่า, server รีบูท, ดีพลอยพังก์) ด้วย งานจะกลับมาใช้งานได้เมื่อเวลาผ่านไป\n\nกฎทั่วไป: งานสามารถถูกจับได้เมื่อ เป็น หรือ \n\n### จับงานด้วยการอัพเดตแบบอะตอมเดียว\n\nรายละเอียดสำคัญคือจับงานในคำสั่งเดียว (หรือหนึ่งธุรกรรม) คุณต้องการให้ฐานข้อมูลเป็นผู้ตัดสิน\n\nนี่คือลายพบบ่อยบน PostgreSQL: เลือกงานที่ถึงเวลา ล็อกมัน และคืนมาให้ worker (ตัวอย่างนี้ใช้ตาราง เดียว แนวคิดเดียวกันใช้กับ )\n\n\n\nเหตุผลที่มันทำงาน:\n\n- ให้ worker หลายตัวแข่งกันโดยไม่บล็อกกัน\n- lease ถูกเซ็ตเมื่อจับงาน ดังนั้น worker ตัวอื่นจะไม่สนใจจนกว่าจะหมดอายุ\n- ส่งแถวให้ worker ที่ชนะการแข่ง\n\n### ระยะเวลา lease ควรเป็นเท่าไหร่ และจะต่ออายุอย่างไร\n\nตั้ง lease ให้ยาวกว่าการรันปกติ แต่สั้นพอที่การ crash จะกลับมาฟื้นตัวได้เร็ว หากงานส่วนใหญ่เสร็จใน 10 วินาที ให้ lease 2 นาทีก็พอสำหรับหลายกรณี\n\nสำหรับงานยาว ให้ต่ออายุ lease ระหว่างทำงาน (heartbeat) วิธีง่าย ๆ คือทุก 30 วินาที ขยาย หากคุณยังเป็นเจ้าของงานอยู่\n\n- ความยาว lease: 5x ถึง 20x เวลาทั่วไปของงาน\n- ระยะ heartbeat: 1/4 ถึง 1/2 ของ lease\n- คำสั่งต่ออายุควรมี \n\nเงื่อนไขสุดท้ายสำคัญ มันป้องกันไม่ให้ worker ขยาย lease บนงานที่มันไม่ได้เป็นเจ้าของแล้ว\n\n## การลองใหม่และ backoff ให้คาดเดาได้\n\nการลองใหม่คือจุดที่รูปแบบนี้จะทำให้คุณสงบหรือกลายเป็นความวุ่นวาย เป้าหมายคือเรียบง่าย: เมื่องานล้มเหลว ให้ลองใหม่ในภายหลังในวิธีที่คุณอธิบาย, วัดผล, และหยุดได้\n\nเริ่มจากทำให้สถานะงานชัดและจำกัด: , , , , ในงานจริงทีมมักใช้ เพื่อหมายถึง “ล้มเหลวแต่จะลองใหม่” และ หมายถึง “ล้มเหลวและยอมแพ้” ความต่างนี้ป้องกันลูปไม่รู้จบ\n\nการนับ attempts เป็นการป้องกันชั้นที่สอง เก็บ (จำนวนครั้งที่ลอง) และ (จำนวนครั้งที่ยอมให้ลอง) เมื่อ worker จับข้อผิดพลาด ควร:\n\n- เพิ่ม \n- ตั้งสถานะเป็น หาก มิฉะนั้น \n- คำนวณ สำหรับครั้งถัดไป (เฉพาะกรณี )\n\nbackoff คือกฎที่กำหนด ถัดไป เลือกแบบหนึ่งแล้วจงสม่ำเสมอและบันทึก:\n\n- หน่วงคงที่: รอเสมอ 1 นาที\n- กำลังสอง (exponential): 1m, 2m, 4m, 8m\n- exponential พร้อมเพดาน: exponential แต่ไม่เกิน เช่น 30m\n- ใส่ jitter: ทำให้เวลาเล็กน้อยเป็นแบบสุ่มเพื่อไม่ให้งานลองใหม่พร้อมกันทั้งหมด\n\nJitter สำคัญเมื่อ dependency ล่มแล้วกลับมา หากไม่มีมัน งานหลายร้อยงานจะลองพร้อมกันและล้มอีก\n\nเก็บรายละเอียดข้อผิดพลาดพอให้มองเห็นและดีบักได้ ไม่จำเป็นต้องมีระบบล็อกเต็มรูปแบบ แต่ต้องมีพื้นฐาน:\n\n- (ข้อความสั้น แสดงในจอแอดมินได้)\n- หรือ (ช่วยจัดกลุ่ม)\n- และ \n- แบบออฟชันัล (ถ้าควบคุมขนาดได้)\n\nกฎที่ใช้ได้ดี: ทำ หลัง 10 attempts แล้ว backoff แบบ exponential พร้อม jitter วิธีนี้ทำให้ความล้มเหลวชั่วคราวถูกลองใหม่ แต่หยุดงานที่พังจริง ๆ จากการใช้ CPU ตลอดไป\n\n## Idempotency: ป้องกันผลซ้ำแม้งานจะรันซ้ำ\n\nIdempotency หมายถึงงานของคุณสามารถรันซ้ำแล้วยังให้ผลสุดท้ายเหมือนเดิม ในรูปแบบนี้มันสำคัญเพราะแถวเดียวอาจถูกหยิบขึ้นมาซ้ำหลัง crash, timeout, หรือ retry หากงานของคุณคือ “ส่งอีเมลใบแจ้งหนี้” การรันซ้ำไม่ใช่เรื่องเล็กเสมอไป\n\nวิธีปฏิบัติคือแยกงานเป็น (1) ทำงาน และ (2) ประยุกต์ผลลัพธ์ คุณต้องการให้ผลลัพธ์เกิดขึ้นครั้งเดียว แม้ว่างานครั้งแรกจะพยายามหลายครั้ง\n\n### ใช้ idempotency key ที่ผูกกับเหตุการณ์ธุรกิจ\n\nidempotency key ควรมาจากสิ่งที่งานแทนค่า ไม่ใช่จากความพยายามของ worker คีย์ที่ดีคือคีย์ที่คงที่และอธิบายง่าย เช่น , , หรือ หากสองความพยายามของงานอ้างถึงเหตุการณ์โลกจริงเดียวกัน ควรใช้คีย์เดียวกัน\n\nตัวอย่าง: “สร้างรายงานขายรายวันสำหรับ 2026-01-14” ใช้ “เรียกเก็บเงิน invoice 812” ใช้ \n\n### บังคับ “ครั้งเดียวเท่านั้น” ด้วยข้อจำกัดฐานข้อมูล\n\nการป้องกันที่ง่ายที่สุดคือให้ PostgreSQL ปฏิเสธรายการซ้ำ เก็บ idempotency key ที่สามารถทำดัชนีได้ แล้วเพิ่ม unique constraint\n\n\n\nมันป้องกันไม่ให้มีสองแถวที่มีคีย์เดียวกันอยู่พร้อมกัน หากออกแบบให้มีหลายแถว (เพื่อเก็บประวัติ) ให้ใส่ uniqueness บนตาราง "effects" แทน เช่น หรือ \n\nผลข้างเคียงที่ควรปกป้อง:\n\n- อีเมล: สร้างแถว ที่มีคีย์ unique ก่อนส่ง หรือลง provider message id เมื่อส่งแล้ว\n- Webhooks: บันทึก และข้ามถ้ามีแล้ว\n- การชำระเงิน: ใช้ idempotency ของ provider พร้อม unique key ในฐานข้อมูลของคุณเอง\n- การเขียนไฟล์: เขียนเป็นชื่อชั่วคราวแล้วเปลี่ยนชื่อ หรือบันทึก โดยคีย์ \n\nถ้าคุณสร้างบนสแตกที่ใช้ Postgres (เช่น backend Go + PostgreSQL) การตรวจสอบความเป็นเอกลักษณ์พวกนี้เร็วและทำใกล้ข้อมูลได้ ความคิดหลักคือ: retry เป็นเรื่องปกติ แต่การซ้ำซ้อนเป็นเรื่องที่ต้องเลือก\n\n## ขั้นตอนทีละขั้น: สร้าง worker และ scheduler ขั้นพื้นฐาน\n\nเลือก runtime เดี่ยวที่เชื่อถือได้และยึดตามมัน จุดมุ่งหมายของรูปแบบ cron + ฐานข้อมูลคือลดชิ้นส่วนที่ต้องดูแล ดังนั้น process เล็ก ๆ ใน Go, Node, หรือ Python ที่คุยกับ PostgreSQL ก็เพียงพอ\n\n### สร้างใน 5 ขั้นเล็ก ๆ\n\n1) เพิ่มตาราง (และตาราง lookup เพิ่มเติมถ้าต้องการ) จากนั้นทำดัชนี และดัชนีที่ช่วย worker ค้นหางานที่พร้อมได้เร็ว (เช่น ).\n\n2) แอปของคุณควร insert แถวโดยมี เป็น หรือเวลาในอนาคต เก็บ payload ให้เล็กและคาดเดาได้ (ID และ job type ไม่ใช่ blob ใหญ่)\n\n\n\n3) รันมันใน transaction เลือกงานที่ถึงเวลา ล็อกพวกมันเพื่อให้ worker คนอื่นข้าม และตั้งเป็น ใน transaction เดียวกัน\n\n\n\n4) สำหรับแต่ละงานที่จับได้ ทำงาน แล้วอัพเดตเป็น พร้อม หากล้มเหลว ให้บันทึกข้อความ error และเลื่อนไปเป็น พร้อม ใหม่ (backoff) ทำให้การอัพเดตสรุปเล็กและทำเสมอ แม้กระทั่งเมื่อ process กำลังปิดตัว\n\n5) ใช้สูตรง่าย ๆ เช่น และหยุดหลัง โดยตั้ง \n\n### เพิ่มการมองเห็นพื้นฐาน\n\nไม่จำเป็นต้องมี dashboard เต็มรูปแบบตั้งแต่วันแรก แต่ต้องพอให้เห็นปัญหา\n\n- โลกหนึ่งบรรทัดต่อ job: claimed, succeeded, failed, retried, dead\n- สร้าง query/view ง่าย ๆ สำหรับ “dead jobs” และ “running jobs เก่านาน”\n- แจ้งเตือนเมื่อจำนวนเพิ่ม (เช่น มากกว่า N dead jobs ในชั่วโมงที่ผ่านมา)\n\nถ้าคุณอยู่บนสแต็ก Go + PostgreSQL สิ่งนี้จับคู่ได้ดีกับ binary worker เดียวบวก cron\n\n## ตัวอย่างสมจริงที่คัดลอกได้\n\nจินตนาการแอป SaaS ขนาดเล็กที่มีงานตามกำหนดสองอย่าง:\n\n- การล้างข้อมูลรายคืนที่ลบ sessions หมดอายุและไฟล์ชั่วคราวเก่า\n- อีเมล “รายงานกิจกรรมของคุณ” รายสัปดาห์ส่งให้ผู้ใช้ทุกเช้าวันจันทร์\n\nทำให้ง่าย: ตาราง PostgreSQL เดียวเก็บ jobs และ worker ตัวเดียวรันทุกนาที (trigger โดย cron) Worker จับงานที่ถึงเวลา, รันมัน, และบันทึกความสำเร็จหรือความล้มเหลว\n\n### อะไรถูก enqueue และเมื่อไหร่\n\nคุณสามารถ enqueue งานจากหลายที่:\n\n- ทุกวันเวลา 02:00: enqueue งาน หนึ่งงานสำหรับ “วันนี้”\n- เมื่อสมัคร: enqueue งาน สำหรับผู้ใช้ในจันทร์ถัดไป\n- หลังเหตุการณ์ (เช่น “ผู้ใช้คลิก Export report”): enqueue งาน ที่รันทันทีสำหรับช่วงวันที่เฉพาะ\n\npayload คือสิ่งจำเป็นขั้นต่ำที่ worker ต้องการ เก็บให้เล็กเพื่อ retry ง่าย\n\n\n\n### idempotency ป้องกันการส่งซ้ำอย่างไร\n\nworker อาจ crash ในช่วงแย่ที่สุด: หลังส่งอีเมลแล้วแต่ก่อนจะมาร์กงานเป็น “done” เมื่อมันรีสตาร์ท มันอาจหยิบงานเดิมขึ้นมาอีกครั้ง\n\nเพื่อหยุดการส่งซ้ำ ให้การทำงานมีคีย์ dedupe ตามธรรมชาติและเก็บไว้ในที่ที่ฐานข้อมูลบังคับได้ สำหรับรายงานประจำสัปดาห์ คีย์ที่ดีคือ ก่อนส่ง worker บันทึกว่า “ฉันกำลังจะส่งรายงาน X” ถ้ารายการนั้นมีอยู่แล้ว ให้ข้ามการส่ง\n\nสิ่งนี้อาจเป็นตาราง ที่มี unique constraint บน หรือ แบบ unique บน job เอง\n\n### ลักษณะความล้มเหลว (และการกู้คืน)\n\nสมมติ provider ส่งอีเมล timeout งานล้มเหลว worker จะ:\n\n- เพิ่ม \n- บันทึกข้อความ error เพื่อดีบัก\n- กำหนดเวลาลองใหม่ด้วย backoff (เช่น: +1 นาที, +5 นาที, +30 นาที, +2 ชั่วโมง)\n\nถ้ามันยังล้มเหลวเกินขีดจำกัด (เช่น 10 attempts) ให้มาร์กเป็น “dead” แล้วหยุด retry งานนั้นก็จะสำเร็จครั้งหนึ่งหรือมันจะลองใหม่ตามตารางที่ชัดเจน และ idempotency ทำให้การ retry ปลอดภัย\n\n## ข้อผิดพลาดและกับดักที่พบบ่อย\n\nรูปแบบ cron + ฐานข้อมูลเรียบง่าย แต่ความผิดพลาดเล็ก ๆ น้อย ๆ อาจทำให้เกิดการซ้ำ, งานติด, หรือโหลดที่ไม่คาดคิด ปัญหาส่วนใหญ่ปรากฏหลัง crash, ดีพลอย, หรือสไปค์การใช้งานครั้งแรก\n\n### ข้อผิดพลาดที่ทำให้เกิดการซ้ำหรือการติดงาน\n\nเหตุการณ์จริงมักมาจากกับดักไม่กี่ข้อ:\n\n- รันงานเดียวกันจากหลายรายการ cron โดยไม่มี lease ถ้าสองเซิร์ฟเวอร์ติ๊กในนาทีเดียวกัน ทั้งคู่อาจพยายามจับงานเดียวกันหากขั้นตอนการจับไม่ใช่แบบอะตอมและไม่ตั้งล็อก/lease ใน transaction เดียว\n- ข้าม ถ้า worker crash หลังจับงาน แถวอาจอยู่ในสถานะ “กำลังประมวลผล” ตลอดไป timestamp ของ lease ช่วยให้ worker อื่นรับงานได้ในภายหลัง\n- ลองใหม่ทันทีเมื่อเกิดความล้มเหลว เมื่อ API ล่ม instant retry สร้างสไปค์ เผาผลาญ rate limit และยังคงล้มในลูปหนาแน่น ให้กำหนดเวลาลองใหม่ไปข้างหน้าเสมอ\n- ถือว่า “at least once” เป็น “exactly once” งานอาจวิ่งสองครั้ง (timeout, worker restart, network) หากการรันสองครั้งเป็นอันตราย ให้ทำให้ผลข้างเคียงสามารถทำซ้ำได้อย่างปลอดภัย\n- เก็บ payload ใหญ่ในแถวงาน บลอบ JSON ใหญ่พองโตตาราง ช้าในการดัชนี และทำให้การล็อกหนักขึ้น จงเก็บ reference (เช่น , , หรือ file keyNOW()run_atlocked_untilrun_atstatusattemptslocked_untilmax_attemptslast_errorinvoice_idmax_attemptsrun_at = now()(status, run_at)`)\n\nถ้าคุณต้องการสร้างเซ็ตอัพแบบนี้เร็ว ๆ Koder.ai (koder.ai) สามารถช่วยจากสคีมาไปจนถึงแอป Go + PostgreSQL ที่ deploy ได้ โดยลดงานเชื่อมต่อนิดหน่อย ให้คุณโฟกัสที่ล็อก, การ retry, และกฎ idempotency\n\nถ้าคุณเติบโตเกินขอบเขตนี้ภายหลัง คุณยังได้เรียนรู้วงจรชีวิตของงานอย่างชัดเจน และแนวคิดเดียวกันนี้ก็แปลงไปใช้กับระบบคิวเต็มรูปแบบได้ดี