ซิงก์ยอดเงินระหว่างผลิตภัณฑ์: แสดงยอดเรียลไทม์ของอีกผลิตภัณฑ์ผ่าน WebSocket
เมื่อผลิตภัณฑ์ A ย้ายยอดเงินของผู้ใช้ไปยังผลิตภัณฑ์ B และแอปของ A ต้องแสดงยอดของ B การโพลผ่านแบ็กเอนด์ของ A ทำให้กำลังของ A ขึ้นกับความหน่วงของ B ผังงานแสดงว่าการให้ B พุชทุกการเปลี่ยนแปลงผ่าน WebSocket แก้ปัญหาได้อย่างไร
รูปแบบที่พบบ่อยระหว่างสองผลิตภัณฑ์: ผู้ใช้ย้ายยอดเงินบางส่วนจากผลิตภัณฑ์ A ไปยังผลิตภัณฑ์ B แล้วใช้ใน B ระหว่างที่ผู้ใช้ใช้จ่ายหรือได้เงินใน B แอปของ A ยังต้องแสดงแบบเรียลไทม์ว่าใน B เหลือเท่าไร เวอร์ชันแรกมักถามผ่าน HTTP โดยแอปของ A โพลแบ็กเอนด์ของ A และแบ็กเอนด์ของ A ไปถาม B ทุกการโพลจึงกลายเป็นสายโซ่คำขอที่ข้ามทั้งสองผลิตภัณฑ์ และเมื่อทราฟฟิกโตขึ้น สิ่งที่จำกัดว่าคุณรองรับผู้ใช้ได้กี่คนไม่ใช่ผลิตภัณฑ์ใด แต่เป็นสายโซ่นี้
สายโซ่คำขอสะสมขึ้นมาอย่างไร
ย้ายยอดเงิน
แสดงยอดของ B ในแอปของ A
คำขอ HTTP ทุกคำขอจะยึดบางอย่างไว้ระหว่างรอ ได้แก่ เธรด worker การเชื่อมต่อจากพูลของไคลเอนต์ และบ่อยครั้งรวมถึงการเชื่อมต่อฐานข้อมูล คำขอของ A ตอบไม่ได้จนกว่า B จะตอบ จึงยึดสิ่งเหล่านี้ไว้นานเท่ากับงานของตัวเองบวกเวลาทั้งหมดที่ B ใช้ กฎของลิตเติลช่วยให้คำนวณได้: จำนวนคำขอที่กำลังทำงาน = คำขอต่อวินาที × วินาทีที่แต่ละคำขอใช้ และ B ได้รับคำขอจากผู้ใช้ A ทุกคนที่เปิดหน้าอยู่ทุกไม่กี่วินาที ไม่ว่ายอดจะเปลี่ยนหรือไม่
ต้องใช้ worker เท่าไร
นี่คือตัวอย่างการคำนวณ ไม่ใช่ผลการทดสอบ (เขียนเมื่อ 2026-11-09): ผู้ใช้ของ A 5,000 คนเปิดยอดของ B ไว้บนหน้าจอ และแอปของ A โพลทุก 5 วินาที จึงมีคำขอ 1,000 ครั้งต่อวินาทีผ่าน A ไปยัง B
| โพลผ่านแบ็กเอนด์ของ A | B พุชทุกการเปลี่ยนแปลงผ่าน WebSocket | |
|---|---|---|
| คำขอยอดเงินต่อวินาทีที่ถึง A และ B | ฝั่งละ 1,000 ครั้ง ส่วนใหญ่ไม่เปลี่ยน | ไม่มี อ่านครั้งเดียวตอนแอปเชื่อมต่อ |
| เวลาที่แต่ละคำขอยึด worker ของ A | งานของตัวเอง 10 ms + เรียก B 60 ms = 70 ms | ไม่ผ่าน A เลย |
| worker ของ A ที่ไม่ว่างเพราะส่งต่อยอดของ B | 1,000 × 0.07 วินาที = 70 | 0 |
| เมื่อ B ช้าลงเป็น 600 ms | 1,000 × 0.61 วินาที = 610 เป็นสามเท่าของพูล 200 | ยังคง 0: อัปเดตถัดไปแค่มาช้าลง |
แถวสุดท้ายคือจุดที่เจ็บ ถ้าพูลมี 200 worker แค่ B ช้าลงชั่วขณะก็ใช้พูลของ A หมดได้ คำขอของ A เอง รวมถึงการล็อกอินและการชำระเงิน ต้องรอในคิว หมดเวลา แล้วไคลเอนต์ลองใหม่ ทำให้คำขอเพิ่มขึ้นอีก การทำงานพร้อมกันของ A ลดลงตรงจังหวะที่ทราฟฟิกสูงสุด เพราะผลิตภัณฑ์ที่ A ไม่ได้ดูแลเอง ขณะเดียวกัน B ก็เสียกำลังไปกับการตอบโพลที่ส่วนใหญ่บอกว่า "ไม่มีอะไรเปลี่ยน"
หลัง: B เผยแพร่แต่ละการเปลี่ยนแปลงเพียงครั้งเดียว
B เผยแพร่หนึ่งครั้งหลังทุกการเปลี่ยนแปลงที่เขียน รวมถึงการโอนเข้า แอปของ A เปิดการเชื่อมต่อ WebSocket ไว้หนึ่งเส้นและได้รับยอดใหม่ภายในเวลาเดินทางเครือข่ายหนึ่งรอบ ยอดของ B ไม่ผ่านแบ็กเอนด์ของ A อีกต่อไป B ที่ช้าหรือยุ่งจึงยึด worker ของ A ไม่ได้ และ B ก็ไม่ต้องตอบโพลนับพันครั้งอีก
วิธีตั้งค่า
- ยอดของ B เป็นของ B บัญชีของ B คือแหล่งข้อมูลจริงว่าใน B เหลือเท่าไร หลังทุกการคอมมิต B เผยแพร่
balance.updatedพร้อมยอดใหม่และหมายเลขเวอร์ชัน ผ่าน HTTP API ของไลบรารีเซิร์ฟเวอร์ Pusher ตัวใดก็ได้ - หนึ่งแชนเนลส่วนตัวต่อผู้ใช้ เช่น
private-b-balance-42ถ้า A และ B ใช้แอปเดียวกัน endpoint ยืนยันสิทธิ์ของ A จะเซ็นการสมัครรับหลังตรวจว่าผู้ใช้ที่ล็อกอินคือ 42 ถ้า B มีแอปของตัวเอง แอปของ A จะเชื่อมต่อด้วยคีย์แอปของ B และ endpoint ยืนยันสิทธิ์ของ B เป็นผู้เซ็น ใช้private-encrypted-b-balance-42ถ้าตัวส่งต่อควรเห็นแค่ข้อความเข้ารหัส แชนเนลส่วนตัวอื่น ๆ ได้รับการปกป้องด้วย TLS ระหว่างส่ง และเข้ารหัสเมื่อจัดเก็บ - ใช้หมายเลขเวอร์ชัน ข้ามอีเวนต์ที่เวอร์ชันต่ำกว่าที่แสดงบนหน้าจอ เมื่อแอปของ A เชื่อมต่อหรือเชื่อมต่อใหม่ ให้อ่านยอดของ B ผ่าน HTTP หนึ่งครั้ง แล้วจึงใช้อีเวนต์
- แบ็กเอนด์ของ A ก็รับได้ ถ้า A ต้องใช้ยอดของ B บนเซิร์ฟเวอร์ เช่น ก่อนให้ผู้ใช้ย้ายเงินกลับ ก็เก็บสำเนาที่อัปเดตจากอีเวนต์เดียวกันด้วยไลบรารีไคลเอนต์ Pusher แทนการเรียก B ทุกครั้ง
สิ่งที่ยังเหมือนเดิม
การย้ายจาก A ไป B ยังเป็นคำขอ HTTP พร้อม idempotency key การลองใหม่จึงไม่ย้ายเงินซ้ำ และ B ยังตรวจยอดเองทุกครั้งที่เขียน WebSocket นำข่าวว่ายอดของ B เปลี่ยน ไม่ได้นำตัวการเปลี่ยนแปลง ถ้าผลิตภัณฑ์อีกฝั่งเป็นผู้ให้บริการ API ภายนอก การโอนเองก็ทำให้คิวล้นได้ ดูการโอนวอลเล็ตกับผู้ให้บริการ API
คำถามที่พบบ่อย
ทำไมไม่โพลให้ห่างขึ้นแทน?
ช่วงที่ยาวขึ้นแปลว่าคำขอน้อยลงแต่ยอดเก่าลง เงินที่ได้ใน B จะโผล่ในแอปของ A ช้าไปหนึ่งช่วงเต็ม และทุกการโพลก็ยังต้องรอ B การพุชใช้เวลาเดินทางเครือข่ายหนึ่งรอบ และไม่ส่งอะไรเลยเมื่อไม่มีอะไรเปลี่ยน
ถ้าแอปของ A พลาดอีเวนต์ระหว่างหลุดการเชื่อมต่อล่ะ?
pusher-js และไลบรารีไคลเอนต์อื่น ๆ เชื่อมต่อใหม่เอง หลังเชื่อมต่อใหม่ ให้อ่านยอดของ B ผ่าน HTTP หนึ่งครั้ง แล้วใช้เฉพาะอีเวนต์ที่เวอร์ชันสูงกว่า หมายเลขเวอร์ชันยังทำให้อีเวนต์ซ้ำหรือมาช้าไม่เป็นปัญหา
A ต้องแก้โค้ดของ B หรือไม่?
B ต้องเพิ่มเพียงอย่างเดียว คือเผยแพร่ balance.updated หลังเขียนยอด และเซ็นการสมัครรับถ้ามีแอปของตัวเอง API การโอนยังเหมือนเดิม
Hibira นับอีเวนต์เหล่านี้อย่างไร?
การเผยแพร่หนึ่งครั้งนับเป็นหนึ่งข้อความ แผนแบบชำระเงินไม่นับการส่งถึงผู้สมัครรับ ยอดเดียวกันที่แสดงทั้งในแอปของ A และ B จึงมีค่าใช้จ่ายเท่ากับแสดงในแอปเดียว ดูราคา
ชื่อผลิตภัณฑ์เป็นเครื่องหมายการค้าของเจ้าของ