原先每 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%。
30 lines
940 B
Bash
Executable File
30 lines
940 B
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
db_path="${1:-LionWebsite.db}"
|
|
|
|
if [[ ! -f "$db_path" ]]; then
|
|
echo "数据库不存在: $db_path" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet lionwebsite 2>/dev/null; then
|
|
echo "检测到 lionwebsite.service 仍在运行;请先停止服务再迁移,避免写冲突。" >&2
|
|
exit 3
|
|
fi
|
|
|
|
if pgrep -f 'java .*lionwebsite\.jar' >/dev/null 2>&1; then
|
|
echo "检测到主站进程仍在运行;请先停止服务再迁移,避免写冲突。" >&2
|
|
exit 3
|
|
fi
|
|
|
|
backup="${db_path}.before-refresh-schedule.$(date -u +%Y%m%dT%H%M%SZ)"
|
|
cp "$db_path" "$backup"
|
|
echo "已备份: $backup"
|
|
|
|
sqlite3 "$db_path" < "$(dirname "$0")/migrate_subscription_refresh.sql"
|
|
|
|
echo "--- 校验 ---"
|
|
sqlite3 "$db_path" "select count(*) as accounts, sum(next_refresh_at is null) as missing_schedule from subscription_account;"
|
|
echo "订阅刷新计划迁移完成: $db_path"
|