← หัวข้อ

ซิงก์ยอดเงินระหว่างผลิตภัณฑ์: แสดงยอดเรียลไทม์ของอีกผลิตภัณฑ์ผ่าน WebSocket

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

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

สายโซ่คำขอสะสมขึ้นมาอย่างไร

ย้ายยอดเงิน

แอปของ A
POST /transfer
แบ็กเอนด์ของ Aรอ B
POST /deposit
แบ็กเอนด์ของ B

แสดงยอดของ B ในแอปของ A

แอปของ Aทุก 5 วินาที
GET /b-balance
แบ็กเอนด์ของ Aรอ B
GET /balance
แบ็กเอนด์ของ Bหนึ่ง worker ต่อคำขอ
แอปของ Aทุก 5 วินาที
GET /b-balance
แบ็กเอนด์ของ Aรอ B
GET /balance
แบ็กเอนด์ของ Bหนึ่ง worker ต่อคำขอ
ก่อน: ทุกผู้ใช้ทุกไม่กี่วินาที แอปของ A ถามแบ็กเอนด์ของ A แล้วแบ็กเอนด์ของ A ไปถาม B

คำขอ HTTP ทุกคำขอจะยึดบางอย่างไว้ระหว่างรอ ได้แก่ เธรด worker การเชื่อมต่อจากพูลของไคลเอนต์ และบ่อยครั้งรวมถึงการเชื่อมต่อฐานข้อมูล คำขอของ A ตอบไม่ได้จนกว่า B จะตอบ จึงยึดสิ่งเหล่านี้ไว้นานเท่ากับงานของตัวเองบวกเวลาทั้งหมดที่ B ใช้ กฎของลิตเติลช่วยให้คำนวณได้: จำนวนคำขอที่กำลังทำงาน = คำขอต่อวินาที × วินาทีที่แต่ละคำขอใช้ และ B ได้รับคำขอจากผู้ใช้ A ทุกคนที่เปิดหน้าอยู่ทุกไม่กี่วินาที ไม่ว่ายอดจะเปลี่ยนหรือไม่

ต้องใช้ worker เท่าไร

นี่คือตัวอย่างการคำนวณ ไม่ใช่ผลการทดสอบ (เขียนเมื่อ 2026-11-09): ผู้ใช้ของ A 5,000 คนเปิดยอดของ B ไว้บนหน้าจอ และแอปของ A โพลทุก 5 วินาที จึงมีคำขอ 1,000 ครั้งต่อวินาทีผ่าน A ไปยัง B

แถวสุดท้ายคือจุดที่เจ็บ ถ้าพูลมี 200 worker แค่ B ช้าลงชั่วขณะก็ใช้พูลของ A หมดได้ คำขอของ A เอง รวมถึงการล็อกอินและการชำระเงิน ต้องรอในคิว หมดเวลา แล้วไคลเอนต์ลองใหม่ ทำให้คำขอเพิ่มขึ้นอีก การทำงานพร้อมกันของ A ลดลงตรงจังหวะที่ทราฟฟิกสูงสุด เพราะผลิตภัณฑ์ที่ A ไม่ได้ดูแลเอง ขณะเดียวกัน B ก็เสียกำลังไปกับการตอบโพลที่ส่วนใหญ่บอกว่า "ไม่มีอะไรเปลี่ยน"

หลัง: B เผยแพร่แต่ละการเปลี่ยนแปลงเพียงครั้งเดียว

แอปของ A
POST /transfer
แบ็กเอนด์ของ A
POST /deposit
แบ็กเอนด์ของ Bเขียนบัญชีของ B
แบ็กเอนด์ของ Bหลังทุกการเปลี่ยนแปลง
เผยแพร่ balance.updated
Hibiraprivate-b-balance-42
WebSocket
แอปของ Aและแอปของ B เอง
หลัง: B เผยแพร่ทุกการเปลี่ยนแปลงยอดของผู้ใช้ และแอปของ A รับโดยตรง
แอปของ Aแบ็กเอนด์ของ Aแบ็กเอนด์ของ BHibiraย้าย 1,000 ไป BPOST /depositidempotency key200ยอดใน B 1,000 เวอร์ชัน 1balance.updatedprivate-b-balance-42balance.updated1,000 เวอร์ชัน 1ต่อมาผู้ใช้ใช้ไป 120 ใน Bbalance.updated880 เวอร์ชัน 2balance.updated880 เวอร์ชัน 2
ผู้ใช้ย้าย 1,000 ไป B แล้วใช้ไป 120 ใน B แอปของ A เห็นทั้งสองครั้งทันที

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 จึงมีค่าใช้จ่ายเท่ากับแสดงในแอปเดียว ดูราคา

ชื่อผลิตภัณฑ์เป็นเครื่องหมายการค้าของเจ้าของ