Files
lionwebsite-backend/scripts/migrate_subscription_refresh.sql
root 3adb42c538 订阅刷新改为每账号错峰,保证 24 小时内各刷一次
原先每 24 小时一次性刷新全部子账号:12 个账号在 16 秒内背靠背发完
24 个请求,且 @Scheduled(fixedRate) 未设 initialDelay,首次触发即立刻
执行,导致每次重启都重刷一遍全部账号——上游看到的请求密度实际取决于
部署频率。请求头也只带 HttpClient 默认值,上游收到的是
"Apache-HttpClient/5.6.4 (Java/25.0.4)"。

改为高频轻量 tick(5 分钟,延迟 2 分钟启动),只刷新已到期账号,单轮
最多 2 个。24 小时保证由三层构成:成功后按「成功时刻 + 窗口 − 一个 tick」
推进相位,使分散效果自我维持;到期时刻持久化在数据库,停机期间错过的
账号恢复后立即到期;兜底把被写到超过一个窗口之后的计划判为损坏并立即
刷新。失败用固定间隔重试(数据库没有连续失败次数字段,做不出真正的指数
退避),重试排在将来时被尊重,避免故障账号每个 tick 重打上游。从未成功过
的账号按 tick 间隔尽快刷,已有缓存的按窗口分散。超过两倍窗口仍未成功的
账号推送告警。

请求头按账号与订阅格式确定性地伪装成 mihomo/clash-verge/v2rayN 等真实
客户端身份;同一账号固定用同一身份,逐请求更换反而更像机器。实现前已
实测确认显式设置 Accept-Encoding 不会破坏 HttpClient5 的透明解压。注意
这只能伪装到 HTTP 头,TLS 指纹仍与真实客户端不同。

新增 next_refresh_at、last_success_epoch 两列,均为 Epoch 毫秒整数:SQLite
的 CURRENT_TIMESTAMP 写 UTC 文本而被 JDBC 按本地时区解释,实测偏差 8 小时,
不能用于时间比较。迁移为纯新增列,旧二进制忽略它们,故回滚程序无需回滚
数据库;回填 last_success_epoch 必须执行,否则既有账号会被当成从未成功过
而集中补刷,正好复现本次要消除的爆发。

同时移除 LocalService 的 24 小时全量入口与 refreshAll()(它们正是爆发式
写法),并将调度线程池由默认 1 调到 2,避免刷新阻塞连接自检。

测试 439 通过,指令覆盖率 82.1%。
2026-09-15 16:15:41 +08:00

27 lines
1.5 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 为每账号分散刷新增加计划字段。
-- 纯新增列:旧版本二进制忽略这两列,因此回滚程序时不必回滚数据库。
BEGIN;
-- next_refresh_at:该子账号下一次应刷新的时刻,Epoch 毫秒。
-- 用整数存时刻,避免 SQLite 文本时间戳被按本地时区解释(实证存在 8 小时偏差)。
-- last_success_epoch:最近一次成功刷新的真实时刻,Epoch 毫秒。
-- 既有的 last_success_at 保持原样,仅供页面展示,不参与调度判断。
ALTER TABLE subscription_account ADD COLUMN next_refresh_at INTEGER;
ALTER TABLE subscription_account ADD COLUMN last_success_epoch INTEGER;
-- 回填 last_success_epoch:SQLite 的 strftime 把文本时间按 UTC 解释,而
-- CURRENT_TIMESTAMP 写入的正是 UTC,因此这里能得到正确时刻(JDBC 读取则会偏 8 小时)。
-- 必须回填:否则所有既有账号都会被当成「从未成功过」,迁移后立刻集中补刷一遍,
-- 正好复现本次改造要消除的爆发。
UPDATE subscription_account
SET last_success_epoch = CAST(strftime('%s', last_success_at) AS INTEGER) * 1000
WHERE last_success_epoch IS NULL AND last_success_at IS NOT NULL;
-- 已有账号在窗口内按 id 错开,避免迁移后同一 tick 集中开火。
-- 以迁移时刻为基准按 37 分钟步长铺开(12 个账号约 7.4 小时排完)。
UPDATE subscription_account
SET next_refresh_at = (CAST(strftime('%s', 'now') AS INTEGER) * 1000) + (id * 37 * 60 * 1000)
WHERE next_refresh_at IS NULL;
COMMIT;