价格提醒:v1(单层)→ v2(两层)

同一个功能,换了一种分工。用户看到的界面和接口几乎没变,变的是「谁去盯价格」。

一句话:v1 是每个用户各雇一个人盯行情;v2 是全公司只雇一个人拿着清单盯行情,谁的价位到了就打电话通知谁。
盯价的工作量从「随用户数增长」变成「固定一份」,代价是多了一个跨 DO 的通知环节,要自己保证别漏、别重复。

1. 打个比方

v1:每人一个盯盘员 单层

1 万个用户 = 1 万个盯盘员。每 10 秒,1 万人同时去交易所看一眼同一个大屏,再各自回去对自己老板的清单。

看的是同一块屏,却看了 1 万次。

v2:门口一个总盯盘员 两层

总盯盘员手里一张大清单:「张三 BTC 涨破 10 万」「李四 ETH 跌破 2000」…… 每 10 秒看一次大屏,对一遍清单,只给价位到了的人打电话。

没被打电话的用户,完全不用醒。

2. 结构图

v1 单层 Hyperliquid API 用户 DO 0x1111 alerts 表 ⏰ 自己的 Alarm 用户 DO 0x2222 alerts 表 ⏰ 自己的 Alarm 用户 DO … alerts 表 ⏰ 自己的 Alarm 红线:每用户各拉一次 N 个用户 = N 个闹钟 = 每 10s N 次请求 v2 两层 Hyperliquid API 价格源 DO「global」 watches 索引表 ⏰ 全服务唯一的 Alarm 10s 一次 用户 DO 0x1111 alerts 表 · WS 用户 DO 0x2222 💤 睡着 用户 DO … 💤 睡着 ① watch() ② 命中才 onFired() N 个用户 = 1 个闹钟 = 每 10s 1 次请求

3. 主要区别

v1 单层 v2 两层
谁去拉 HL 价格 每个用户 DO 自己拉 只有价格源 DO 拉
Alarm 个数 每个有未触发提醒的用户 1 个 全服务 1 个
谁判断「命中没」 用户 DO 对自己的清单 价格源对全体清单(watches 表)
用户 DO 什么时候醒 每 10 秒都醒 只在加提醒、查列表、连 WS、真的命中时醒
DO 类 PriceAlerts PriceAlerts + 新增 PriceFeed(migration v2)
数据 只有 alerts alerts(不变)+ watches(冗余一份 coin/方向/价格)
新增提醒 写本地 + 挂闹钟 写本地 + 去价格源登记,登记失败就撤销本地
触发 本地改状态、推送,同一个 DO 里一气呵成 价格源跨 DO 回调 onFired,成功才删索引,失败下轮重试
不变的 HTTP 接口、WebSocket 推送、alerts 表结构、网页用法

4. 为什么要这样拆

① 重复劳动

allMids 一次就返回所有币的价格。v1 里 1 万个用户拿到的是同一份数据,却拉了 1 万次。

② 钱

DO 按请求、时长、写行数计费。v1 每个用户每 10 秒都要:1 次 Alarm 请求 + 1 次 HL 往返的时长 + 续闹钟写 1 行。v2 这些全服务只付一份。

③ 外部限频

v1 的出站请求数随用户数线性增长,迟早撞上 HL 的 IP 限频(具体额度未核实)。v2 固定每 10 秒 1 次。

④ 用户 DO 更纯粹

用户 DO 只管「我的数据 + 我的连接」,不再关心行情从哪来。以后换成订阅 HL WebSocket,只改价格源。

成本粗算(Paid 计划,超出免费额度后;时长按每次 100ms 估,未实测)

v1:每个活跃用户每月 v2:整个服务每月(固定部分)
Alarm 请求 ≈ 26 万次 ≈ $0.04 ≈ $0.04
续闹钟写行 ≈ 26 万行 ≈ $0.26 ≈ $0.26
时长 ≈ 3250 GB-s ≈ $0.04 ≈ $0.04
合计 ≈ $0.34 × 用户数(1 万人 ≈ $3400) ≈ $0.34 + 每次命中 1 次回调请求

v2 另有按量的小头:每条新提醒多一次 watch() RPC 和写 1 行;每次命中多一次 onFired() RPC。

5. 拆了之后要付的代价

两个库,没有事务

alerts 和 watches 在不同 DO 的不同 SQLite 里,不能一起提交。所以代码里有「登记失败撤销本地」「先通知、后删索引」「onFired 幂等」三道保险。

数据冗余

价格源存一份 coin/方向/价格,命中判断才不用去问用户。以后加「修改提醒」就得两边同步。

新热点

价格源是单实例,所有盯价都压在它身上。量大了按币拆:getByName("feed:BTC")。

多一跳

命中到推送多一次跨 DO RPC,延迟多几毫秒到几十毫秒(量级估计,未实测),对 10 秒一轮的提醒无所谓。

6. 升级时怎么接住旧数据

v1 已经在跑的用户 DO 身上还挂着旧闹钟。v2 的用户 DO 把 alarm() 改成了「迁移钩子」:旧闹钟最后响一次时,把自己还没触发的提醒补登记到价格源,之后不再续闹钟。

实测:本地重启后价格源一启动就有 2 条,正是 v1 时代 0x1111 和 0x3dc3 那两条没触发的提醒。

另一个顺带学到的点:价格源在 await fetch(HL) 时,别的请求(比如网页查 /feed)可以插进来,读到「闹钟已被平台清掉、还没续上」的中间状态。所以加了 checking 标记,面板显示「检查中…」。DO 的单线程保证的是同一时刻只有一段代码在跑,不保证一个方法从头到尾不被打断。