- 退避上限从 5 秒压到 1.5 秒(含抖动最长约 1.9 秒),连续失败放大倍数 8→4,抖动 50%→25%: 2026-10-03 实测库存只活了约 1.1 秒(PC2 四个实例 18:00:00.171~18:00:01.101 一直能锁到 同一个礼包),而 PC1 默认实例的 31 个号在 18:00:00.225 失败后一直睡到 18:01:09.379, 唯一有货的那段窗口被退避吃掉;超过 2 秒的等待都可能正好错过库存回流 - 首发请求不再随机等 150-700 毫秒,改为"第 0 个账号立即发出 + 其余按 8ms 步长错开", 整批铺开不超过 300 毫秒(账号再多也不会拖长)。定时器本来就提前约 100ms 触发以补偿 RTT, 再抖几百毫秒等于把这个提前量整个抵消,请求实际落到 t+150ms~t+650ms - 锁单成功到首次支付的等待从 2-4 秒压到 300-700 毫秒:这段纯属把关键路径上的时间白送出去, "订单操作进行中"现在按"已受理、结果未定"快重试,不需要靠等待规避 - 支付退避上限同步收紧:频控 3s→2s,其它 2s→1.2s(在途仍为 300ms→1s) - 新增 TestRetryDelayNeverBacksOffLong:遍历 75 秒 × 40 次连续失败,断言任何一次退避都 小于 2 秒,并覆盖"重试间隔策略被配到 5000ms"的极端配置 - 新增 TestFirstWaveStartsImmediately:单账号步长必须为 0、位次从 0 递增、 2/34/100/500/2000 个账号下步长 ≤8ms 且整批铺开 ≤300ms - 设置页文案同步:退避"任何一次等待都不超过约 2 秒"、首发错开说明
159 lines
4.2 KiB
Go
159 lines
4.2 KiB
Go
package seckill
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// runControl 承载一次抢购任务的共享状态:窗口截止时间、并发闸门、请求计数。
|
|
//
|
|
// 手册链路(精英手册锁单支付 / 电竞手册直接兑换)下,每个账号在各自的 goroutine
|
|
// 中独立重试,互不阻塞。并发闸门作用在"单次 HTTP 请求"上而不是整个账号循环上,
|
|
// 这样账号在退避等待期间不会占着并发额度。
|
|
type runControl struct {
|
|
s *Scheduler
|
|
start time.Time
|
|
deadline time.Time
|
|
stop <-chan struct{}
|
|
limit chan struct{}
|
|
|
|
firstWaveStep time.Duration
|
|
|
|
mu sync.Mutex
|
|
lockReqs int
|
|
payReqs int
|
|
exchangeReqs int
|
|
firstWaveUsed int
|
|
}
|
|
|
|
// newRunControl 创建运行控制块。concurrency 限制同时在途的请求数,
|
|
// accountCount 用来决定首发请求的错开步长。
|
|
func newRunControl(s *Scheduler, start time.Time, deadline time.Time, concurrency, accountCount int) *runControl {
|
|
if concurrency <= 0 {
|
|
concurrency = 1
|
|
}
|
|
return &runControl{
|
|
s: s,
|
|
start: start,
|
|
deadline: deadline,
|
|
stop: s.stopRunChan(),
|
|
limit: make(chan struct{}, concurrency),
|
|
firstWaveStep: firstWaveStepFor(accountCount),
|
|
}
|
|
}
|
|
|
|
// firstWaveStepFor 计算首发请求之间的错开步长。
|
|
//
|
|
// 首发是整场抢购最关键的一波,不能像早期版本那样随机等 150-700ms——定时器本来就
|
|
// 提前约 100ms 触发以补偿 RTT,再抖几百毫秒等于把提前量整个抵消。
|
|
// 这里让第 0 个账号立即发出,其余按步长依次错开,整批最多铺开 firstWaveSpread。
|
|
func firstWaveStepFor(accountCount int) time.Duration {
|
|
if accountCount <= 1 {
|
|
return 0
|
|
}
|
|
step := firstWaveSpread / time.Duration(accountCount-1)
|
|
if step > firstWaveMaxStep {
|
|
step = firstWaveMaxStep
|
|
}
|
|
if step < 0 {
|
|
step = 0
|
|
}
|
|
return step
|
|
}
|
|
|
|
// firstWaveSlot 领取本账号的首发错开位次与步长。
|
|
func (c *runControl) firstWaveSlot() (int, time.Duration) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
slot := c.firstWaveUsed
|
|
c.firstWaveUsed++
|
|
return slot, c.firstWaveStep
|
|
}
|
|
|
|
// enter 占用一个并发额度;返回 false 表示任务已被停止。
|
|
func (c *runControl) enter() bool {
|
|
select {
|
|
case c.limit <- struct{}{}:
|
|
return true
|
|
default:
|
|
}
|
|
select {
|
|
case c.limit <- struct{}{}:
|
|
return true
|
|
case <-c.stop:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// leave 释放并发额度。
|
|
func (c *runControl) leave() {
|
|
<-c.limit
|
|
}
|
|
|
|
// stopped 返回是否已收到停止指令。
|
|
func (c *runControl) stopped() bool {
|
|
return c.s.runStopped()
|
|
}
|
|
|
|
// expired 返回抢购窗口是否已经结束(结束后不再发起新的锁单)。
|
|
func (c *runControl) expired() bool {
|
|
return !time.Now().Before(c.deadline)
|
|
}
|
|
|
|
// elapsed 返回任务开始至今的时间,用于按配置的重试阶段取节奏。
|
|
func (c *runControl) elapsed() time.Duration {
|
|
return time.Since(c.start)
|
|
}
|
|
|
|
// sleep 在抢购窗口内等待,等待时间会被剩余窗口裁剪。
|
|
// 返回 false 表示该账号应当结束循环(窗口结束或收到停止指令)。
|
|
func (c *runControl) sleep(d time.Duration) bool {
|
|
return c.sleepUntil(c.deadline, d) && !c.stopped()
|
|
}
|
|
|
|
// sleepUntil 在指定截止时间前等待。
|
|
// 返回 false 表示到达截止时间或收到停止指令,调用方应当结束当前流程。
|
|
//
|
|
// 支付阶段用锁单过期时间作为截止,而不是抢购窗口:锁单已经占用库存,
|
|
// 窗口结束后仍要把已锁订单支付完,中途放弃等于白锁。
|
|
func (c *runControl) sleepUntil(limit time.Time, d time.Duration) bool {
|
|
if d > 0 {
|
|
remaining := time.Until(limit)
|
|
if remaining <= 0 {
|
|
return false
|
|
}
|
|
if d > remaining {
|
|
d = remaining
|
|
}
|
|
if c.s.interruptibleSleep(d) {
|
|
return false
|
|
}
|
|
}
|
|
return time.Now().Before(limit) && !c.stopped()
|
|
}
|
|
|
|
func (c *runControl) countLockReq() {
|
|
c.mu.Lock()
|
|
c.lockReqs++
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *runControl) countPayReq() {
|
|
c.mu.Lock()
|
|
c.payReqs++
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *runControl) countExchangeReq() {
|
|
c.mu.Lock()
|
|
c.exchangeReqs++
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// counts 返回锁单 / 支付 / 直接兑换的真实请求次数,用于汇总日志。
|
|
func (c *runControl) counts() (lock, pay, exchange int) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.lockReqs, c.payReqs, c.exchangeReqs
|
|
}
|