先说结论:预约登记丢单,绝大多数不是网络差,而是"同一秒几十人点提交 + 服务端没有防抖和重试"造成的。高峰期一次性涌进来,接口超时、客户端拿到错误码就放弃、用户重新点又重复提交——一套组合拳下来,订单丢了、数据还乱了。本文用一次真实的轻应用预约登记事故复盘,给出防抖、限流、重试三套可落地的方案和代码。
背景交代下环境:给一家做同城便民服务的客户落地过轻应用预约登记,基础的表单、数据入库由某一站式SaaS底座承载,日常单量下很稳;但"活动报名开放"这种瞬间高并发,是SaaS通用能力覆盖不到的场景,需要在业务层自己加防抖和重试逻辑。这次事故就发生在这个自研的业务层。
某天上午10点开放报名,10点整300多人同时点提交。后端日志显示:10:00:00-10:00:05 五秒内涌入212个请求,接口平均响应从80ms涨到2.3秒,13%的请求超时返回500。客户端没有重试逻辑,用户看到"提交失败"就反复点,又制造了一波重复请求。最终核对:实际报名260人,后台只落了213条,丢了47单,还混着19条重复数据。
根因拆开就三件事:①没有防抖,同一用户连点两次提交了两次;②没有限流,瞬间流量全打到数据库;③没有重试,超时的请求直接丢弃,前端还不感知。
最便宜的第一道防线。按钮点击后立即置灰,1.5秒内不允许再点;同时给提交按钮加一个内存锁:
let submitting = false;
async function submitReserve(form) {
if (submitting) return; // 防重复提交
submitting = true;
btn.disabled = true; // 按钮置灰
try {
await api.post('/reserve', form);
} finally {
submitting = false;
btn.disabled = false;
}
}效果:连点问题直接消失。这一层能拦掉大约30%的无效请求(都是手滑连点)。
前端防抖挡不住"300人同时来"这种真实并发,服务端必须限流。用令牌桶或最简单的固定窗口计数:
import time
from collections import deque
class WindowLimiter:
def __init__(self, limit, window=1.0):
self.limit = limit # 每秒最多放行数
self.window = window
self.requests = deque()
def allow(self):
now = time.time()
while self.requests and self.requests[0] < now - self.window:
self.requests.popleft()
if len(self.requests) >= self.limit:
return False
self.requests.append(now)
return True
limiter = WindowLimiter(limit=50) # 每秒50个
if not limiter.allow():
return {"code": 429, "msg": "当前报名人数过多,请稍后再试"}超限的请求不硬拒,返回429让前端走"稍后重试"或排队提示,避免全部压进数据库。实测这一层把数据库QPS从212/5s压到50/s以内,接口不再雪崩。
限流之后依然有少量超时(弱网、慢客户端),必须有重试机制。重试的关键是幂等——同一个用户同一个活动的提交,无论重试多少次,后台只落一条:
import hashlib
from uuid import uuid4
def build_idem_key(user_id, activity_id, ts):
return hashlib.md5(f"{user_id}:{activity_id}:{ts}".encode()).hexdigest()
# 前端提交时带 idem_key(首次生成,重试复用)
def reserve(user_id, activity_id, form, idem_key=None):
idem_key = idem_key or build_idem_key(user_id, activity_id, int(time.time()))
# 数据库唯一索引兜底:重复提交命中唯一键,返回已存在的记录
# INSERT INTO reserve (idem_key, ...) VALUES (...) ON CONFLICT (idem_key) DO NOTHING;
return idem_key客户端配合指数退避重试:300ms → 900ms → 2700ms,最多3次。超时的单子在重试后补交成功,配合幂等键不产生重复数据。这一层把丢单率从15%降到0.3%。
这三层不是三选一,是按顺序叠的:前端防抖先拦连点 → 服务端限流保数据库 → 重试+幂等补丢单。跳层会出问题——只做限流不做重试,限流拦掉的单子全丢了;只做重试不做幂等,重试制造重复数据;只做防抖不做限流,300人同时涌来还是会把接口打垮。叠满三层,压测数据是:5秒300并发下,成功提交率99.7%,重复数据0条,接口P99从2.3秒降到320ms。
方案上线后不是不管了,每天看三个数:①丢单率(提交成功数/落库数,>1%就要查);②重复率(幂等键命中数/总提交数,突然升高说明有入口漏了幂等);③接口P99耗时(超过1秒检查是不是限流阈值设高了)。这三个数都正常,预约登记基本不会出事。
Q1:预约登记丢单一定是后端问题吗?
答:多数不是。先看客户端有没有防抖和重试——前端连点、超时不重试,是丢单的两大来源;后端限流和幂等是第二道防线,三层都查一遍。
Q2:限流会不会把正常用户拦在外面?
答:限流阈值按峰值流量的1.5-2倍设(本例300并发峰值→每秒50放行),正常用户不受影响;超限的返回429+排队提示,不是硬失败。
Q3:重试和重复提交怎么区分?
答:靠幂等键。同一用户同一活动同一时间片生成一个幂等键,重试复用同一个键,数据库唯一索引兜底——重复提交命中唯一键,直接返回已存在记录,不重复落库。
Q4:这套方案能直接套到别的表单场景吗?
答:能。报名、预约、问卷、领券,凡是"瞬间高并发+表单提交",都是防抖+限流+重试+幂等四件套,参数按业务量调就行。
预约登记的稳定性,不是"网络好"就够的,是防抖、限流、重试三层各司其职的结果:前端拦连点,服务端拦并发,重试补丢单,幂等防重复。三层都配齐,300人同时报名也能稳住;少一层,丢单和重复就找上门。这套逻辑不只在预约登记上有效,任何"瞬间高并发 + 表单提交"的场景都适用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。