大流量场景踩坑:前端如何应对秒杀活动的并发请求
在秒杀、抢购、票务开售等大流量场景下,前端会面对瞬时并发涌入、用户高频点击、网络抖动与后端限流等复杂情况。本文从踩坑角度整理前端侧的通用应对策略与落地代码片段,帮助减少重复提交、抖动请求、级联雪崩和页面卡顿。
问题背景
- 瞬时峰值并发增大:大量用户在同一时间点触发关键操作(下单、抢券、锁库存)。
- 弱网与高延迟:移动网络抖动导致请求超时、重试放大。
- 后端限流与队列:网关/服务端对异常流量进行丢弃或排队,返回非 2xx。
- 浏览器并发限制:同域连接数限制、队头阻塞、资源竞争。
常见踩坑
- 重复点击导致多次下单或多次扣减库存。
- 页面多个组件各自发起同一接口请求,造成风暴式重复调用。
- 无抖动的固定重试间隔,形成同步峰值,进一步压垮后端。
- 无取消机制,用户切页或重复操作仍保留大量悬挂请求。
- 乐观更新没有回滚,出现"下单成功 UI"但服务端失败的错觉。
设计原则
- 收敛请求:相同语义的并发请求尽量合并为一次或共享结果。
- 有界并发:关键路径采用队列化或信号量控制上限。
- 可取消与超时:避免僵尸请求占用资源。
- 幂等与一致性:在重试、失败回滚中保持数据与 UI 一致。
- 抖动与退避:避免同步重试造成流量高频对齐。
应对策略
防重复点击与操作节流
- 在关键按钮上设置短期"操作锁":一次提交完成前禁止再次触发。
- 配合节流/防抖控制密集点击,仅触发一次有效请求。
请求去重与合并(Request Coalescing)
- 为相同参数的请求维护
inflight映射,后续并发请求直接复用首个 Promise。 - 多组件共享同一数据时,通过全局缓存/数据层统一调度。
客户端限流与背压
- 信号量/令牌桶限制并发数量,关键路径串行化或低并发执行。
- 对非关键请求(日志、预加载)采用低优先级队列或丢弃策略。
队列化与关键路径串行化
- 将"下单/锁库存"等关键操作队列化,确保一次只处理一个任务。
取消与超时控制
- 使用
AbortController主动取消过期请求,搭配自定义超时器。
幂等与乐观更新
- 请求携带幂等键(如一次性令牌、客户端唯一操作 ID),避免重复扣减。
- 乐观更新需在失败时回滚 UI 状态,保证一致性。
重试策略与抖动(Exponential Backoff with Jitter)
- 对可重试错误采用指数退避并引入随机抖动,降低同步对齐概率。
Service Worker 与离线队列(可选)
- 对非关键写操作谨慎使用离线队列;关键下单不建议离线排队。
安全与防刷协作
- 与后端协作引入一次性令牌、签名校验、交互验证(滑块/点击验证码)。
与后端协作要点
- 幂等接口:支持幂等键防重复执行。
- 令牌与窗口:下单令牌有效期与窗口限制,防止脚本刷。
- 排队与票据:对"秒杀资格"进行排队发券,前端按票据进行一次性提交。
- 清晰错误码:区分可重试与不可重试,指导前端策略。
参考代码片段
1. Inflight 去重共享
js
const inflight = new Map();
function keyOf(url, opts) {
return `${url}::${JSON.stringify(opts || {})}`;
}
async function fetchOnce(url, opts) {
const key = keyOf(url, opts);
if (inflight.has(key)) return inflight.get(key);
const p = fetch(url, opts).finally(() => inflight.delete(key));
inflight.set(key, p);
return p;
}
2. 信号量限制并发
js
class Semaphore {
constructor(max = 2) { this.max = max; this.cur = 0; this.q = []; }
async acquire() { return new Promise(r => { this.cur < this.max ? (this.cur++, r()) : this.q.push(r); }); }
release() { const n = this.q.shift(); n ? n() : this.cur--; }
}
const sem = new Semaphore(1); // 关键路径串行
async function guarded(fn) {
await sem.acquire();
try { return await fn(); } finally { sem.release(); }
}
3. 取消与超时
js
function withTimeout(ms, controller) {
const id = setTimeout(() => controller.abort(), ms);
return () => clearTimeout(id);
}
async function request(url, opts = {}, timeout = 5000) {
const c = new AbortController();
const clear = withTimeout(timeout, c);
try { return await fetch(url, { ...opts, signal: c.signal }); }
finally { clear(); }
}
4. 指数退避 + 抖动重试
js
async function retry(fn, { tries = 4, base = 200 } = {}) {
let attempt = 0;
while (true) {
try { return await fn(); }
catch (e) {
attempt++;
if (attempt >= tries) throw e;
const jitter = Math.random() * base;
const delay = Math.min(3000, base * 2 ** (attempt - 1) + jitter);
await new Promise(r => setTimeout(r, delay));
}
}
}
5. 令牌桶限速(客户端)
js
class TokenBucket {
constructor({ rate = 1, burst = 3 }) {
this.rate = rate; this.burst = burst; this.tokens = burst; this.last = performance.now();
}
tryTake() {
const now = performance.now();
const dt = (now - this.last) / 1000;
this.tokens = Math.min(this.burst, this.tokens + dt * this.rate);
this.last = now;
if (this.tokens >= 1) { this.tokens -= 1; return true; }
return false;
}
}
const bucket = new TokenBucket({ rate: 0.5, burst: 2 });
function guardedClick(handler) {
return (...args) => bucket.tryTake() && handler(...args);
}
6. 操作锁与乐观更新回滚
js
let lock = false;
async function submitOnce(doSubmit) {
if (lock) return;
lock = true;
try {
// 乐观更新开始
setSubmitting(true);
const res = await doSubmit();
// 成功:保持状态
return res;
} catch (e) {
// 失败:回滚 UI
setSubmitting(false);
throw e;
} finally { lock = false; }
}
压测与可观测性
- 在预演环境进行峰值压测,验证并发上限、超时、重试策略是否合理。
- 埋点请求量、失败比、平均时延、95/99 分位、取消次数等指标。
- 预留开关:在流量异常时快速降低并发、关闭自动重试。
检查清单
- 关键按钮是否有操作锁与节流。
- 相同请求是否去重合并,是否共享结果。
- 是否有超时与取消,避免僵尸请求。
- 是否实现有界并发(信号量/队列)。
- 重试是否具备退避与抖动,避免同步风暴。
- 是否与后端约定幂等键与错误码分类。
- 是否具备完整埋点与切换开关。
总结
前端在秒杀等高并发场景的核心目标是"控流、收敛、可取消、可回滚"。通过请求去重、并发上限、取消与超时、退避重试、幂等与乐观更新,配合后端令牌与幂等接口,可以显著降低风暴式请求与一致性问题,提升用户体验与系统稳定性。