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

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

Source: https://hibira.io/th/topics/balance-sync-websocket
Updated: 2026-11-09

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

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

```text
flow: ก่อน: ทุกผู้ใช้ทุกไม่กี่วินาที แอปของ 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 ต่อคำขอ
```

คำขอ HTTP ทุกคำขอจะยึดบางอย่างไว้ระหว่างรอ ได้แก่ เธรด worker การเชื่อมต่อจากพูลของไคลเอนต์ และบ่อยครั้งรวมถึงการเชื่อมต่อฐานข้อมูล คำขอของ A ตอบไม่ได้จนกว่า B จะตอบ จึงยึดสิ่งเหล่านี้ไว้นานเท่ากับงานของตัวเองบวกเวลาทั้งหมดที่ B ใช้ [กฎของลิตเติล](https://en.wikipedia.org/wiki/Little%27s_law)ช่วยให้คำนวณได้: จำนวนคำขอที่กำลังทำงาน = คำขอต่อวินาที × วินาทีที่แต่ละคำขอใช้ และ 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 เผยแพร่แต่ละการเปลี่ยนแปลงเพียงครั้งเดียว

```text
flow: หลัง: B เผยแพร่ทุกการเปลี่ยนแปลงยอดของผู้ใช้ และแอปของ A รับโดยตรง
แอปของ A --POST /transfer--> แบ็กเอนด์ของ A --POST /deposit--> แบ็กเอนด์ของ B|เขียนบัญชีของ B
แบ็กเอนด์ของ B|หลังทุกการเปลี่ยนแปลง --เผยแพร่ balance.updated--> *Hibira|private-b-balance-42 --WebSocket--> แอปของ A|และแอปของ B เอง
```

```text
sequence: ผู้ใช้ย้าย 1,000 ไป B แล้วใช้ไป 120 ใน B แอปของ A เห็นทั้งสองครั้งทันที
แอปของ A | แบ็กเอนด์ของ A | แบ็กเอนด์ของ B | *Hibira
1 > 2: ย้าย 1,000 ไป B
2 > 3: POST /deposit | idempotency key
3 >> 2: 200 | ยอดใน B 1,000 เวอร์ชัน 1
3 > 4: balance.updated | private-b-balance-42
4 > 1: balance.updated | 1,000 เวอร์ชัน 1
note: ต่อมาผู้ใช้ใช้ไป 120 ใน B
3 > 4: balance.updated | 880 เวอร์ชัน 2
4 > 1: balance.updated | 880 เวอร์ชัน 2
```

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](https://hibira.io/th/topics/wallet-transfers-api-providers)

## คำถามที่พบบ่อย

### ทำไมไม่โพลให้ห่างขึ้นแทน?

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

### ถ้าแอปของ A พลาดอีเวนต์ระหว่างหลุดการเชื่อมต่อล่ะ?

pusher-js และไลบรารีไคลเอนต์อื่น ๆ เชื่อมต่อใหม่เอง หลังเชื่อมต่อใหม่ ให้อ่านยอดของ B ผ่าน HTTP หนึ่งครั้ง แล้วใช้เฉพาะอีเวนต์ที่เวอร์ชันสูงกว่า หมายเลขเวอร์ชันยังทำให้อีเวนต์ซ้ำหรือมาช้าไม่เป็นปัญหา

### A ต้องแก้โค้ดของ B หรือไม่?

B ต้องเพิ่มเพียงอย่างเดียว คือเผยแพร่ `balance.updated` หลังเขียนยอด และเซ็นการสมัครรับถ้ามีแอปของตัวเอง API การโอนยังเหมือนเดิม

### Hibira นับอีเวนต์เหล่านี้อย่างไร?

การเผยแพร่หนึ่งครั้งนับเป็นหนึ่งข้อความ แผนแบบชำระเงินไม่นับการส่งถึงผู้สมัครรับ ยอดเดียวกันที่แสดงทั้งในแอปของ A และ B จึงมีค่าใช้จ่ายเท่ากับแสดงในแอปเดียว ดู[ราคา](https://hibira.io/th/pricing)
