高并发不是后端的专利:一个前端仔的系统设计觉醒之路
去年双11前两周,我们组被临时塞进一个“保障重点项目”——一个秒杀活动页。作为组里资历最老(其实也就干了快两年)的前端,我本以为就是切切页面、调调动画、怼几个Lottie就完事了。结果第一次需求对齐会上,后端同事冷不丁甩出一句:“前端要扛住每秒5万QPS的压力。”
我当场懵了。QPS?那不是Java老哥们在K8s集群里折腾的东西吗?我一个写React + TypeScript 的前端,连Nginx配置都只敢看不敢改,怎么突然要操心高并发了?
但现实很骨感。产品经理拍着胸脯说“这个活动必须爆”,运维大哥幽幽补刀:“服务器资源有限,能省则省”,而测试同学已经准备好用JMeter轰我们的接口了。那一刻我意识到:现代前端,早就不只是UI层了。
从“AI写代码?达咩!”到真香现场
坦白讲,我之前对AI辅助编程是极度抵触的。总觉得那是偷懒、是糊弄、是对代码不负责任。直到上周五晚上十点,我还在死磕一个Redis缓存穿透的问题,本地调试反复失败,线上日志又看不懂。绝望之下,我试着把报错贴给Copilot,它居然几秒内给出了一段带布隆过滤器的预加载逻辑。
虽然最后没直接用,但它让我思路打开了。那一刻我悟了:工具无罪,关键看人怎么用。就像高并发系统,也不是某个语言或角色的专属玩具,而是整个团队协作的结果。今天这篇文章,就是记录我这个前端仔如何被迫“跨界”,从理论到实践摸爬滚打搞高并发的经历。
为什么前端也得懂高并发?
很多人觉得高并发=后端+数据库+中间件,前端只要保证页面不卡就行。但现实是:用户感知的“卡”,往往始于一次失败的请求或雪崩式的重试。
举个真实例子:我们活动页有个倒计时组件,每秒向后端拉取库存状态。初期设计是纯前端轮询,结果活动一开始,瞬间几千人同时请求 /api/stock,后端线程池直接打满,数据库连接数飙到90%,整个服务雪崩。
运维半夜打电话吼我:“你们前端能不能别这么暴力?”
痛定思痛,我们重新梳理了整个链路:
- 前端:减少无效请求、加本地缓存、做请求合并
- 后端(Java):接口限流、缓存击穿防护、异步化处理
- 数据库:读写分离、热点数据分片
这才明白:高并发是系统性工程,前端不是旁观者,而是第一道防线。
实战:从“能跑就行”到“稳如老狗”
第一招:前端削峰填谷
我们把原本每秒一次的轮询,改成 “本地缓存 + 事件驱动” 模式:
// 库存状态管理(简化版)
class StockManager {
private cache: { stock: number; expire: number } | null = null;
private ws: WebSocket | null = null;
async getStock(productId: string): Promise<number> {
// 先查本地缓存(有效期500ms)
if (this.cache && Date.now() < this.cache.expire) {
return this.cache.stock;
}
// 如果没缓存,尝试WebSocket推送(避免HTTP轮询)
if (!this.ws) {
this.connectWebSocket();
}
// 最终兜底:走HTTP(带防抖)
return this.fetchWithDebounce(productId);
}
private fetchWithDebounce(productId: string) {
// 多个组件同时请求,合并为一次
return debounceFetch(productId, 300); // 300ms内重复请求只发一次
}
}
效果立竿见影:前端请求量下降70%,后端压力骤减。
第二招:Java后端的“三板斧”
我们后端用的是Spring Boot + Redis + MySQL。针对高并发场景,他们做了几件关键事:
- 接口限流:用Guava的RateLimiter做单机限流,Sentinel做集群限流
- 缓存策略:
- 热点商品预加载到Redis
- 缓存空值防止穿透
- 设置随机过期时间避免雪崩
- 异步化:下单操作走MQ,前端先返回“排队中”,后台慢慢处理
// Java伪代码:带布隆过滤器的缓存查询
public Product queryProduct(Long id) {
// 1. 布隆过滤器快速判断是否存在
if (!bloomFilter.mightContain(id)) {
return null; // 直接返回,不查DB
}
// 2. 查Redis
Product product = redis.get("product:" + id);
if (product != null) {
return product;
}
// 3. 双重检查锁防止缓存击穿
synchronized (this) {
product = redis.get("product:" + id);
if (product == null) {
product = db.queryById(id);
if (product != null) {
// 随机过期时间:300±30秒
redis.setex("product:" + id, 300 + random.nextInt(60), product);
} else {
// 缓存空值,防止穿透
redis.setex("product:" + id, 60, EMPTY_PLACEHOLDER);
}
}
}
return product;
}
第三招:前后端协同设计接口
以前我们接口设计很随意:“反正前端能拿到数据就行”。现在不行了。我们和Java同事一起制定了 “高并发接口规范”:
| 项目 | 要求 |
|---|---|
| 请求频率 | ≤1次/秒/用户 |
| 数据粒度 | 只返回必要字段,禁止全表SELECT * |
| 错误码 | 明确区分“库存不足”和“系统忙”,前端可做不同提示 |
| 降级策略 | 当服务不可用时,返回静态兜底数据(如“稍后再试”) |
甚至前端主动提出:把倒计时逻辑完全前置到客户端,只在点击“立即抢购”时才发请求。后端直呼“内行”。
血泪教训:那些年我们踩过的坑
缓存雪崩:所有商品缓存同时过期,DB瞬间被打挂。
解法:过期时间加随机偏移(如base + rand(0, 60))前端重试风暴:网络抖动导致请求失败,前端疯狂重试,反而加剧后端压力。
解法:指数退避 + 最大重试次数(如第1次1s后,第2次2s,第3次4s,最多3次)日志爆炸:高并发下每条请求都打日志,磁盘直接写满。
解法:采样日志(如1%请求记录详细日志),关键路径才全量记录跨域问题:CDN加速后,OPTIONS预检请求激增,占用大量带宽。
解法:后端设置Access-Control-Max-Age: 86400,让浏览器缓存CORS结果
性能对比:优化前后数据说话
我们在压测环境模拟了5万并发用户,以下是关键指标对比:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 平均响应时间 | 1200ms | 85ms | ↓93% |
| 错误率 | 23% | 0.4% | ↓98% |
| DB CPU使用率 | 95% | 35% | ↓60% |
| 前端请求量 | 5万/秒 | 1.5万/秒 | ↓70% |
最爽的是双11当天,监控大盘一片绿,运维大哥在群里发了个“👍”,产品经理请我们吃了顿火锅。
写在最后:前端的边界正在消失
两年前我刚入组时,以为前端就是“写页面的”。现在我才懂:现代前端工程师,必须具备系统思维。你不仅要考虑CSS动画的帧率,还要思考请求如何不压垮后端;不仅要会React Hooks,还得懂缓存策略、限流算法、甚至TCP握手。
高并发系统设计,从来不是Java或Go的独角戏。前端通过合理的交互设计、请求策略、本地缓存,完全可以成为系统的“减压阀”。
至于AI写代码?我现在每天用Copilot生成单元测试和文档注释,效率翻倍。果然,真香定律永远成立。
下次再有人说“前端不用懂高并发”,请把这篇文章甩他脸上。当然,如果他是个产品经理……算了,还是请他吃火锅吧,毕竟需求还得做 😅

评论 0