一、百万并发下的惊魂时刻——我的第一个秒杀系统是如何崩溃的?
压测现场实录:
- 10:00:00 请求量瞬间突破50w QPS
- 10:00:03 Redis连接池爆满(2000连接不够用!)
- 10:00:05 MySQL出现大量死锁
- 10:00:07 库存出现-128的灵异现象
此时,我才明白教科书式的减库存代码有多危险:
// 致命错误:非原子操作导致超卖
func deductStock(productID int) bool {
stock := getStockFromDB(productID) // 步骤1
if stock > 0 { // 步骤2
updateStock(productID, stock-1) // 步骤3
return true
}
return false
}
二、血泪换来的4大核心解决方案
2.1 库存超杀防护三件套
正确姿势(基于Redis+Lua原子操作):
script := `local stock = redis.call('get', KEYS[1])
if tonumber(stock) > 0 then
redis.call('decr', KEYS[1])
return 1
else
return 0
end`
val, _ := redisPool.Get().Do("EVAL", script, 1, "product_1001")
2.2 接口限流黑科技(滑动时间窗实现)
// 令牌桶算法实现
type Limiter struct {
tokens chan struct{}
}
func NewLimiter(limit int) *Limiter {
l := &Limiter{tokens: make(chan struct{}, limit)}
go func() {
ticker := time.NewTicker(time.Second / time.Duration(limit))
for range ticker.C {
select {
case l.tokens <- struct{}{}:
default:
}
}
}()
return l
}
三、缓存击穿引发的事故复盘
3.1 缓存雪崩现场
200个商品缓存同时过期,导致数据库连接池被打满:
// 错误缓存读取逻辑
func getProductInfo(id int) Product {
data, err := cache.Get(id)
if err != nil { // 缓存失效
dbResult := queryDB(id) // 直接查库
cache.Set(id, dbResult, 1*time.Minute)
return dbResult
}
//...
}
3.2 双重校验锁的正确姿势
var singleFlight = &singleflight.Group{}
func getProductSafe(id int) Product {
v, err, _ := singleFlight.Do(fmt.Sprint(id), func() (interface{}, error) {
// ... 数据库查询逻辑
})
return v.(Product)
}
四、性能优化前后对比(真实压测数据)
优化项 | 优化前(QPS) | 优化后(QPS) | 资源消耗 |
---|---|---|---|
裸MySQL减库存 | 312 | - | CPU 90%+ |
Redis原子操作 | 12,345 | 48,761 | 内存多消耗15% |
本地缓存+限流 | 8,921 | 78,432 | 网络带宽下降40% |
异步扣库存+MQ确认 | 23,456 | 153,289 | 数据库连接数减少80% |
五、秒杀架构演进路线
- 裸奔型架构(数据库直连)
- 适合:100 QPS以下
- 致命伤:库存不准,易崩溃
- 缓存+限流(Redis+令牌桶)
- 适合:1w QPS以下
- 进阶技巧:本地缓存+二级缓存
- 分层削峰(队列+异步)
- 核心组件:Kafka+RocketMQ
- 关键指标:99.9%请求在200ms内响应
- 全链路优化(CDN预热+动态扩容)
- 杀手锏:弹性计算+自动熔断
- 真实案例:某电商平台支撑过500w QPS
实战建议:
- 压测时务必设置熔断阈值(忘记设置导致K8s集群过载)
- 使用
pprof
定位Go程序内存泄漏(曾因goroutine泄露吃掉32G内存) - 灰度发布时先放1%流量(血的教训:全量发布失败回滚花了47分钟)