跨产品余额同步:用 WebSocket 实时显示另一个产品的余额
产品 A 把用户余额转入产品 B、A 的客户端又要显示 B 的余额时,经由 A 的后端轮询会让 A 的处理能力受制于 B 的延迟。附流程图说明 B 通过 WebSocket 推送每次变动如何解决。
两个产品之间常见的做法:用户把一部分余额从产品 A 转入产品 B,再到 B 里使用。用户在 B 里花掉或赢得金额时,A 的客户端仍然要实时显示 B 里还剩多少。第一版通常用 HTTP 去问:A 的客户端轮询 A 的后端,A 的后端再去问 B。每一次轮询都变成横跨两个产品的请求链,流量一上来,限制你能服务多少用户的就不是任何一个产品,而是这条链。
请求链是怎么叠起来的
转入余额
在 A 的客户端显示 B 的余额
每个 HTTP 请求在等待时都会占住资源:一个 worker 线程、客户端连接池里的一条连接,通常还有一条数据库连接。A 的请求要等 B 响应后才能响应,所以占住这些资源的时间是自己的处理时间,加上 B 花掉的全部时间。利特尔定律可以算出这个数字:处理中的请求数 = 每秒请求数 × 每个请求花的秒数。而且只要有 A 的用户开着页面,B 每隔几秒就会收到一个请求,不管余额有没有变。
要付出多少 worker
以下是推算示例,不是压测结果(写于 2026-11-09):A 有 5,000 名用户的画面上显示着 B 的余额,A 的客户端每 5 秒轮询一次,所以每秒有 1,000 个请求经过 A 送到 B。
| 经由 A 的后端轮询 | B 通过 WebSocket 推送每次变动 | |
|---|---|---|
| 每秒到达 A 和 B 的余额请求 | 各 1,000 个,大多数没有变化 | 没有;客户端连接时读一次 |
| 每个请求占住 A 的 worker 的时间 | 自身 10 ms + 调用 B 的 60 ms = 70 ms | 完全不经过 A |
| A 忙着转送 B 余额的 worker 数 | 1,000 × 0.07 秒 = 70 | 0 |
| B 变慢到 600 ms 时 | 1,000 × 0.61 秒 = 610,是 200 个 worker 连接池的三倍 | 仍然是 0:只是下一次更新晚一点到 |
痛点在最后一列。假设连接池有 200 个 worker,B 一时变慢就足以把 A 的用光:A 自己的请求,包括登录和付款,都在队列里排队、超时,客户端再重试,又加进更多请求。A 的并发量正好在流量高峰时下滑,原因却是一个 A 根本不经营的产品。同时,B 也把自己的处理能力花在响应大多是“没变”的轮询上。
改之后:B 每次变动只发布一次
B 每写入一次变动(包括转入)就发布一次。A 的客户端保持一条 WebSocket 连接,一趟网络往返就收到新余额。B 的余额不再经过 A 的后端,所以变慢或忙碌的 B 占不住 A 的 worker,B 也不用再响应成千上万的轮询。
怎么设置
- B 的余额由 B 负责。 B 的账本是 B 里剩多少的唯一事实来源。每次提交后,B 用任何 Pusher 服务器端库的 HTTP API 发布
balance.updated,带上新余额和版本号。 - 每个用户一个私有频道。 例如
private-b-balance-42。如果 A 和 B 用同一个应用,A 的授权端点确认登录的用户是 42 之后为订阅签名。如果 B 有自己的应用,A 的客户端就用 B 的应用密钥连接,由 B 的授权端点签名。如果中继服务只该看到密文,就用private-encrypted-b-balance-42;其他私有频道在传输中由 TLS 保护,保存时加密。 - 善用版本号。 版本比画面上低的事件直接忽略。A 的客户端连接或重新连接时,先用 HTTP 读一次 B 的余额,再应用之后的事件。
- A 的后端也能收。 如果 A 在服务器上需要 B 的余额,例如让用户把钱转回来之前,可以用 Pusher 客户端库靠同样的事件维护一份副本,不必每次都调用 B。
不变的部分
从 A 转到 B 仍然是带幂等键的 HTTP 请求,重试也绝不会把钱转两次;B 每次写入时也仍会自己检查余额。WebSocket 发送的是“B 的余额变了”这个消息,不是变动本身。如果另一端的产品是外部 API 供应商,转账本身也可能塞爆队列:请看与 API 供应商的钱包转账。
常见问题
为什么不干脆降低轮询频率?
间隔拉长,请求变少,但余额也变旧:在 B 赢得的金额要晚一整个间隔才出现在 A 的客户端,而且每次轮询仍然要等 B。推送一次变动只要一趟网络往返,没有变动时什么都不送。
A 的客户端断线时漏掉事件怎么办?
pusher-js 和其他客户端库会自动重新连接。重新连接后先用 HTTP 读一次 B 的余额,只应用版本更高的事件。版本号也让重复或迟到的事件无害。
A 需要改 B 的代码吗?
B 只要加一件事:写入余额后发布 balance.updated,如果 B 有自己的应用,再为订阅签名。它的转账 API 维持不变。
这些事件在 Hibira 上怎么计算?
每次发布算一条消息。付费方案不计算送给订阅者的次数,所以同一个余额同时显示在 A 和 B 的客户端,费用和只显示在一个客户端相同。请看价格。
产品名称为其所有者的商标。