Files
yml2213 33a2dcf057 feat(sniper): 引入 7.5s~10.5s 黄金回流反扑算法,升级版本至 0.5.4
- 深度剖析 9 秒成交爆发:排除服务端排队假说,确认客户端退避共振与微服务回流/网关开闸叠加
- 优化退避曲线:新增 7.5s~10.5s 黄金回流反扑区,收紧退避并压低连败系数至 1,使单号出拳频次由 1 次提升至 8~10 次
- 同步升级版本号至 v0.5.4(version.go / wails.json / package.json)
- 新增 docs/seckill-9s-burst-analysis.md 沉淀完整分析数据与证据
2026-10-18 21:46:16 +08:00

205 lines
5.7 KiB
Go

package sniper
import (
"time"
)
// sessionLostLimit 返回「连续会话失效多少次后停止该账号」的阈值。
func sessionLostLimit(cfg Config) int {
if cfg.SessionLostLimit > 0 {
return cfg.SessionLostLimit
}
return defaultSessionLostLimit
}
// completeRound 每个账号每轮只更新一次连续失败/会话失效计数。
// 多目标一轮同时失败不会被放大成多次失败;任意可信业务回包都会清零会话失效。
func (s *runState) completeRound(uid int64, categories []string, sessionLimit int) {
s.mu.Lock()
defer s.mu.Unlock()
account := s.accounts[uid]
if account == nil {
return
}
hasSuccess := false
hasRetry := false
hasSessionLost := false
hasSessionAlive := false
for _, category := range categories {
switch category {
case CategorySuccess:
hasSuccess = true
hasSessionAlive = true
case CategorySessionLost:
hasSessionLost = true
default:
if sessionAliveCategory(category) {
hasSessionAlive = true
}
if backoffCategory(category) {
hasRetry = true
}
}
}
if hasSuccess {
account.failStreak = 0
account.sessionStreak = 0
return
}
if hasRetry {
account.failStreak++
}
if hasSessionLost {
account.sessionStreak++
} else if hasSessionAlive {
account.sessionStreak = 0
}
if sessionLimit > 0 && account.sessionStreak >= sessionLimit {
account.done = true
}
}
// roundDelay 返回本轮结束后该账号的统一等待时间:取本轮所有失败分类里
// 最长的退避值,不再为每个 target 单独维护下一次重试时间。
func (s *runState) roundDelay(uid int64, cfg Config, categories []string) time.Duration {
delay := cfg.Delay
if delay <= 0 {
delay = 100 * time.Millisecond
}
seen := make(map[string]bool, len(categories))
for _, category := range categories {
if category == "" || seen[category] || !backoffCategory(category) {
continue
}
seen[category] = true
if candidate := s.retryDelay(uid, cfg, category); candidate > delay {
delay = candidate
}
}
return delay
}
// retryDelay 计算当前账号在某个结局后的退避时间。
func (s *runState) retryDelay(uid int64, cfg Config, category string) time.Duration {
s.mu.Lock()
streak := 0
if account := s.accounts[uid]; account != nil {
streak = account.failStreak
}
s.mu.Unlock()
return computeRetryDelay(s.elapsed(), streak, cfg, category)
}
// computeRetryDelay 使用与 dygou_go 相同的简单阶梯:
// 分类基础值 × 时间阶段 × 连续失败倍率,最后叠加抖动并封顶 1.5 秒。
func computeRetryDelay(elapsed time.Duration, failStreak int, cfg Config, category string) time.Duration {
base := cfg.ErrorBackoff
switch category {
case CategoryTooFrequent:
base = cfg.TooFrequentBackoff
case CategoryNotOpen:
base = maxDuration(cfg.Delay, 50*time.Millisecond)
case CategoryRoleUnbound:
base = maxDuration(cfg.Delay, 200*time.Millisecond)
}
if base <= 0 {
base = defaultRetryBackoff
}
delay := time.Duration(stageMultiplier(elapsed, category)) * base
mult := streakMultiplier(failStreak)
// 在 7.5s ~ 10.5s 黄金回流反扑区(Recovery Burst Window):
// 实测 2026-10-18 该时段超时未核销库存集中释放、网关放宽限流,成交单爆发;
// 压低连败系数至 1,使账号能以基准退避(约 300ms)在 3 秒窗口内密集出拳 8~10 次,
// 避免旧逻辑因 1.5s 封顶退避导致整段回流期单账号只能打出 1 拳的遗憾。
if elapsed >= 7500*time.Millisecond && elapsed <= 10500*time.Millisecond && mult > 1 {
mult = 1
}
if mult > 1 {
delay *= time.Duration(mult)
}
if delay > maxRetryDelay {
delay = maxRetryDelay
}
delay = jitterDuration(delay)
if delay > maxRetryDelay {
delay = maxRetryDelay
}
return delay
}
// elapsed 返回相对开抢时刻的已用时,用于按阶段取退避节奏。
func (s *runState) elapsed() time.Duration {
if !s.openAt.IsZero() {
elapsed := time.Since(s.openAt)
if elapsed < 0 {
return 0
}
return elapsed
}
if s.startedAt.IsZero() {
return 0
}
return time.Since(s.startedAt)
}
// stageMultiplier 按已抢购时长返回退避倍率:
// 开抢头 2 秒最激进(倍率 1);
// 2~5 秒抗频控(倍率 2);
// 7.5~10.5 秒黄金回流反扑区(倍率 1,密集探测回流);
// 10.5 秒之后进入长尾兜底阶段(倍率 3,低频稳步探测)。
func stageMultiplier(elapsed time.Duration, category string) int {
if backoffCategory(category) {
switch {
case elapsed < 2*time.Second:
return 1
case elapsed < 5*time.Second:
return 2
case elapsed >= 7500*time.Millisecond && elapsed <= 10500*time.Millisecond:
return 1
default:
return 3
}
}
if elapsed >= 5*time.Second && (elapsed < 7500*time.Millisecond || elapsed > 10500*time.Millisecond) {
return 2
}
return 1
}
// backoffCategory 返回该结局是否需要账号级退避。
func backoffCategory(category string) bool {
switch category {
case "", CategorySuccess, CategoryNotEnough, CategoryAlreadyDone, CategoryOffShelf:
return false
default:
return true
}
}
// streakMultiplier 把连续失败次数换算成翻倍系数:第 3 次起 ×2,第 4 次起 ×4。
func streakMultiplier(streak int) int {
if streak <= 2 {
return 1
}
mult := 1
for i := 2; i < streak && mult < 4; i++ {
mult <<= 1
}
return mult
}
// sessionAliveCategory 返回该结局是否能证明当前账号的登录态仍然有效。
// 限流、角色服务抖动以及普通业务明确回包均可证明会话存活。
func sessionAliveCategory(category string) bool {
switch category {
case CategorySuccess, CategoryNotOpen, CategorySoldOut, CategoryNotEnough,
CategoryAlreadyDone, CategoryOffShelf, CategoryCondition, CategoryFailed,
CategoryRoleUnbound, CategoryTooFrequent:
return true
default:
return false
}
}