← 专题

跨产品余额同步:用 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 的后端

在 A 的客户端显示 B 的余额

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 花掉的全部时间。利特尔定律可以算出这个数字:处理中的请求数 = 每秒请求数 × 每个请求花的秒数。而且只要有 A 的用户开着页面,B 每隔几秒就会收到一个请求,不管余额有没有变。

要付出多少 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 的后端B 的后端Hibira转 1,000 到 BPOST /deposit幂等键200B 余额 1,000,版本 1balance.updatedprivate-b-balance-42balance.updated1,000,版本 1之后用户在 B 花了 120balance.updated880,版本 2balance.updated880,版本 2
用户转 1,000 到 B,之后在 B 花了 120;A 的客户端两次都立刻看到

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 的客户端,费用和只显示在一个客户端相同。请看价格。

产品名称为其所有者的商标。