原先每 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%。
27 lines
1.5 KiB
PL/PgSQL
27 lines
1.5 KiB
PL/PgSQL
-- 为每账号分散刷新增加计划字段。
|
||
-- 纯新增列:旧版本二进制忽略这两列,因此回滚程序时不必回滚数据库。
|
||
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;
|