- 深度剖析 9 秒成交爆发:排除服务端排队假说,确认客户端退避共振与微服务回流/网关开闸叠加 - 优化退避曲线:新增 7.5s~10.5s 黄金回流反扑区,收紧退避并压低连败系数至 1,使单号出拳频次由 1 次提升至 8~10 次 - 同步升级版本号至 v0.5.4(version.go / wails.json / package.json) - 新增 docs/seckill-9s-burst-analysis.md 沉淀完整分析数据与证据
205 lines
5.7 KiB
Go
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
|
|
}
|
|
}
|