1. 第一次看到这个作业要求,我就知道事情不简单
HCLA第二次作业这个标题,在我电脑里躺了整整三周。这门课的完整名字是High Concurrent Load Application,翻译过来就是高并发负载应用实战,课程目标不是让你背概念,而是逼着你亲手把一个"能跑但很烂"的服务,调优成一个"扛得住压力"的服务。第二次作业的核心任务,是搭建一个模拟秒杀场景的库存扣减服务,要求至少支撑5000并发连接,接口平均响应时间低于500ms,P99响应时间低于1秒,最关键的一条是——库存绝对不能超卖。
当时看到这个要求,我第一反应是"这不就是个普通的后端接口嘛",但真正动手以后才发现,这作业的精髓根本不在业务逻辑,而在于它考验的是你对整个技术栈的理解深度:操作系统资源限制、网络协议栈、并发模型、连接池、缓存一致性、日志IO,哪一块没吃透,高压之下都会露馅。这篇文章我就把从设计、压测、调优到被答辩追问的全过程全部拆开,把我踩过的坑、算过的账、改过的代码,以及最终数据对比都记录下来。如果你也在做类似的并发作业、秒杀项目,或者只是想弄明白"性能优化到底该怎么入手",这篇复盘应该能帮到你。
先说说作业的完整要求。这次作业分为四个交付物:源码(必须带README)、可复现的压测脚本、压测报告(优化前后的对比数据缺一不可)、以及一份不超过15页的复盘文档。老师明确说了一句话,"我不要看你怎么把代码写得天花乱坠,我要看你能不能拿数据讲清楚为什么慢、为什么快"。这句话基本就是整门课的指导思想,也是我后面所有操作的主线。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并发架构选型:为什么我没有一上来就堆线程池
2.1 三种并发模型的实测对比
拿到作业题目之后,第一个拦路虎就是并发模型选型。当时我们班分成了几派:一组坚持用Java的线程池,理由是Spring Boot熟、生态全;一组想用Node.js的事件循环,觉得单线程异步省心;我一开始也犹豫了很久,最后选了Go的goroutine方案。这里我不是说Java不好,而是把三种方案放在"秒杀扣减库存"这个具体场景下做了个对比实测。
先看多线程模型。Java里最经典的做法是FixedThreadPool配合BlockingQueue,把每个请求扔进线程池执行。但线程是有成本的,一个线程默认栈大小1MB,即使真没用到那么多内存,JVM也要为它预留地址空间。实际压测中,我把核心线程数调到200,压到3000并发时,线程上下文切换带来的CPU开销已经非常明显了,top里看到大量sy占比,也就是内核态时间超过30%。更无语的是,任何涉及数据库连接的线程阻塞时,它背着一个完整线程栈在等IO,这1MB内存就白白挂着。
多进程方案我也简单试过,通常都会配合nginx做负载均衡部署多个worker进程。好处是隔离性好,一个进程挂了不影响全局,但进程间通信、共享状态管理成本很高,尤其是扣库存这种高频操作需要共享数据,要么走Redis原子操作,要么走数据库行锁,多了很多网络往返,延迟很难压下去。
然后说Go的goroutine。goroutine的初始栈只有2KB,按需增长,一个4GB内存的实例轻轻松松拉起几万个goroutine。更重要的是Go在语言层面把异步IO封装成了同步写法,你用net/http写一个handler,代码看起来是同步的,底层却是epoll在驱动。这对开发效率的提升是肉眼可见的,不用自己搞回调嵌套,也不用手动管理线程生命周期。
2.2 Go方案的落地细节与踩坑
选型之后就是落地。我的服务结构其实特别简单:一个HTTP接口接收请求,请求参数是用户名和商品ID,然后通过一个Redis Lua脚本完成库存预扣,再异步把扣减结果写入MySQL。整体架构图我在文档里画了,这里不贴图,重点讲代码和坑。
先看初始版本的handler代码骨架:
go复制func (s *Server) handlePurchase(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue("userId")
productID := r.FormValue("productId")
// 检查库存并扣减,走Redis Lua脚本保证原子性
ok, err := s.redis.EvalSha(s.ctx, s.scriptHash, []string{s.stockKey(productID)}, 1).Int()
if err != nil {
http.Error(w, "internal error", 500)
return
}
if ok == 0 {
http.Error(w, "sold out", 400)
return
}
// 异步落库,这里用的是Go channel
s.asyncQueue <- &Order{UserID: userID, ProductID: productID, Time: time.Now()}
w.Write([]byte("ok"))
}
这段代码看起来没问题,但我在做完第一轮压测后发现两个隐患。第一个隐患是asyncQueue这
