高并发系统设计:我在传统企业踩过的坑与填过的雷
上周五晚上十点半,我坐在公司工位上,耳机里放着Lo-fi Hip Hop(写代码的标配BGM),盯着屏幕上不断飙升的CPU使用率,心里默默问候产品经理全家。事情是这样的——我们公司去年开始搞“数字化转型”,领导拍板要做一个在线秒杀活动,目标是支撑每秒3000+请求。听起来不多?但别忘了,我们是个做工业设备的传统制造企业,后端系统还是十年前那套单体架构,数据库连读写分离都没做。
我当时就懵了:这不就是让我用拖拉机去跑F1赛道吗?
但没办法,老板说“这是战略级项目”,还特意点名让我这个“懂点高并发”的Java开发牵头。其实我哪懂什么高并发,只是之前在GitHub上看了几篇Redis和Nginx的文章而已。不过既然上了贼船,那就硬着头皮干吧。今天这篇博客,就是想聊聊这段时间从理论到实践的血泪史,希望能帮到同样在传统企业挣扎的兄弟们。
为什么传统企业搞高并发这么难?
先说说我们的现状。公司主业务系统是用Spring Boot 2.3写的(没错,到现在还没升级到3.x),部署在三台物理机上,MySQL 5.7主从架构(但只有一主一从,而且从库基本不用),缓存?不存在的。接口响应时间动辄800ms+,QPS超过200就开始报警。
更离谱的是,运维大哥说:“你们开发能不能少写点SQL?每次大促数据库就挂。” —— 我心想,你倒是给我加个Redis啊!
在这种环境下谈高并发,简直是“裸奔上战场”。但现实逼着我们得上。双11、618这些电商节日,现在连卖螺丝钉的都要搞促销,我们这种To B企业也得跟风。于是,我拉着两个实习生,开始了这场“不可能完成的任务”。
架构重构:从单体到服务化
第一步,必须拆!原来的系统是典型的“上帝类”设计,一个Service里塞了订单、库存、用户、支付所有逻辑。我花了两周时间,用DDD的思想做了初步拆分,搞出了三个微服务:
- order-service:处理下单逻辑
- stock-service:管理库存扣减
- user-service:用户鉴权和信息查询
技术栈还是Spring Boot,毕竟团队熟悉,学习成本低。但这里我埋了个伏笔——关键路径必须异步化。
比如下单流程,原来是一条线走到底:校验用户 → 扣库存 → 创建订单 → 发短信。现在改成:
- 校验用户(同步)
- 发送“创建订单”消息到Kafka(异步)
- 立即返回“下单成功,请等待确认”
前端配合做了状态轮询,用户体验稍差,但扛住了流量洪峰。实测QPS从200飙到1500+,数据库压力直接降了70%。
// Spring Boot中发送Kafka消息示例
@Service
public class OrderServiceImpl {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public String createOrder(OrderRequest request) {
// 1. 用户校验 & 库存预占(这里用Redis分布式锁)
if (!validateAndPreoccupyStock(request)) {
throw new BizException("库存不足");
}
// 2. 发送异步消息
String orderId = generateOrderId();
OrderMessage msg = new OrderMessage(orderId, request.getUserId(), ...);
kafkaTemplate.send("order-create-topic", JSON.toJSONString(msg));
// 3. 立即返回
return orderId; // 前端根据orderId轮询状态
}
}
这里有个坑:消息可靠性。第一次上线时,Kafka集群配置错了,消息丢了,导致用户付了钱没生成订单。被财务部追着骂了三天。后来加上了消息重试 + 死信队列 + 人工补偿后台,才算稳住。
缓存策略:Redis不是万能药
光靠异步还不够。库存查询这种高频读操作,必须上缓存。我们选了Redis Cluster(3主3从),用Spring Data Redis集成。
但传统企业的数据一致性要求高,不能容忍超卖。所以库存扣减必须先扣DB再更新缓存,而不是反过来。为什么?因为如果先改缓存,DB写失败,缓存就脏了。
// 库存扣减伪代码(带分布式锁)
public boolean deductStock(Long skuId, Integer num) {
String lockKey = "stock_lock:" + skuId;
try (Jedis jedis = pool.getResource()) {
// 1. 获取分布式锁(Lua脚本保证原子性)
if (acquireLock(jedis, lockKey)) {
try {
// 2. 先查DB真实库存
Stock stock = stockMapper.selectById(skuId);
if (stock.getAvailable() >= num) {
// 3. 扣减DB
stockMapper.deduct(skuId, num);
// 4. 更新缓存(设为新值,不是incr/decr)
jedis.setex("stock:" + skuId, 3600,
String.valueOf(stock.getAvailable() - num));
return true;
}
} finally {
releaseLock(jedis, lockKey);
}
}
}
return false;
}
这里用了Redis分布式锁,避免超卖。但锁也有性能损耗,所以只对热点商品(比如秒杀款)加锁,普通商品直接走DB+缓存。
限流熔断:别让系统被冲垮
高并发最怕雪崩。我们用Sentinel做了全链路限流:
- Nginx层:限制单IP每秒请求数
- 网关层(Spring Cloud Gateway):按用户ID限流
- 服务层:核心接口QPS阈值设为2000,超了直接拒绝
# Sentinel规则配置(application.yml)
spring:
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: ${nacos.addr}
data-id: order-service-sentinel-rules
group-id: DEFAULT_GROUP
rule-type: flow
有一次压测,忘了开限流,结果stock-service把DB连接池打满了,整个系统瘫痪。运维大哥冲过来吼:“你们开发是不是又在搞事情?!” —— 自此以后,限流成了上线 checklist 的第一条。
为什么我还偷偷学了Go?
说到这里,不得不提一个敏感话题:Spring Boot真的适合超高并发场景吗?
我们的order-service在峰值时GC停顿高达800ms,虽然用了G1垃圾回收器,但对象分配太快,年轻代撑不住。我试着调优JVM参数,但收效甚微。
于是,我开始偷偷研究Go。原因很简单:Go的goroutine比Java线程轻量太多,而且没有GC停顿问题(虽然Go也有GC,但延迟低得多)。
我用Go重写了库存查询接口(纯读操作),对比测试结果:
| 指标 | Spring Boot (Java 11) | Go (1.20) |
|---|---|---|
| QPS | 2800 | 12000 |
| P99延迟 | 120ms | 18ms |
| 内存占用 | 1.2GB | 80MB |
简直降维打击!但我不敢在生产环境直接上Go——团队没人会维护,出了问题背锅的是我。所以目前只用在非核心的旁路服务,比如日志收集、监控上报。
不过说实话,如果从零开始设计,我会认真考虑Go。尤其是I/O密集型场景,Go的netpoll模型太香了。Spring Boot更适合业务复杂、需要大量生态支持的系统(比如我们现在的主业务),而高并发网关、消息处理这类,Go可能更合适。
数据库优化:最后的防线
即使做了这么多,DB还是瓶颈。我们做了三件事:
- 分库分表:用ShardingSphere-JDBC,按用户ID哈希分16库,订单表按时间分表。
- 读写分离:强制写主库,读走从库(通过Hint强制路由)。
- SQL瘦身:干掉所有
SELECT *,只查必要字段;给user_id + status加复合索引。
最搞笑的是,之前有个接口为了“方便”,在循环里查用户信息:
for (Order order : orders) {
User user = userMapper.selectById(order.getUserId()); // N+1查询!
}
线上直接慢查询爆炸。后来改成批量查询 + Map缓存,接口从2s降到80ms。
安全意识:高并发下的隐秘风险
很多人只关注性能,忽略了安全。高并发场景下,攻击面更大:
- 缓存穿透:恶意查询不存在的ID,打穿到DB。解决方案:布隆过滤器 + 空值缓存。
- 缓存击穿:热点key过期瞬间,大量请求打到DB。解决方案:逻辑过期 + 后台刷新。
- DDoS伪装:攻击者用合法用户身份疯狂刷接口。解决方案:行为分析 + 动态验证码。
我们在网关层加了风控模块,基于用户行为打分。比如1秒内下单10次,直接弹验证码。虽然影响体验,但保住了系统。
效果如何?双11实战检验
今年双11,系统峰值QPS达到3500,平均延迟120ms,0故障。老板在复盘会上夸我:“小王啊,这次做得不错!” —— 其实我心里清楚,功劳是团队的,我只是那个熬夜调参的倒霉蛋。
更重要的是,这次项目让公司意识到:数字化转型不是买几台服务器就行,得从架构、流程、人员全方位升级。现在运维主动找我聊K8s部署,测试也开始写自动化压测脚本,连产品经理都学会说“这个需求会不会影响TPS?”了(笑)。
给同行的几点建议
- 别盲目追求新技术:Spring Boot + Redis + Kafka 这套组合拳,在大多数场景够用了。Go虽好,但要考虑团队能力。
- 压测要贴近真实:用JMeter模拟真实用户行为(包括思考时间、错误率),别只测“理想QPS”。
- 监控必须到位:Arthas看线程堆栈、Prometheus+Grafana看指标、ELK查日志,三位一体。
- 留好逃生通道:开关降级、功能回滚、数据补偿,一个都不能少。
写这篇文章的时候,窗外上海的雨下个不停。我刚改完一个线上Bug,准备下班。回头看看这段高并发之旅,虽然累,但值得。传统企业转型很难,但只要有人愿意迈出第一步,路就会越走越宽。
对了,如果你也在传统企业搞技术,欢迎留言交流。说不定下次团建,我们能在张江的烧烤摊碰上——我请客,只要你别再提“明天上线”就行 😅

评论 0