Commit 720fb45b by 杨树贤

优化版本的

parent 315863bc
......@@ -168,9 +168,29 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
case <-ctxWithTimeout.Done():
// context 超时或被取消
logger.Log("协程整体处理超时,所有子协程已收到取消信号", "sku", 1)
// 等待一小段时间让协程清理资源
time.Sleep(100 * time.Millisecond)
return temp // 超时,返回已经收到的部分数据
// 关键:继续从 channel 读取已经发送的数据,避免协程阻塞
// 设置一个短暂的清理超时
cleanupTimeout := time.After(200 * time.Millisecond)
for {
select {
case GoodsRes, ok := <-ch:
if !ok {
// channel 已关闭,所有协程都已退出
return temp
}
// 收集已经发送的数据
GoodsRes.Range(func(k, v interface{}) bool {
s, _ := k.(string)
temp[s] = v
return true
})
case <-cleanupTimeout:
// 清理超时,强制返回
logger.Log("清理超时,强制返回", "sku", 1)
return temp
}
}
}
}
}
......
# Channel 阻塞导致协程泄漏 - 关键修复说明
# Channel 阻塞导致协程泄漏 - 关键修复说明
## 🔴 问题的真正根源
### 现象
一个请求超时后,后续所有请求都会超时,只有重启服务才能恢复。
### 真正的原因:Channel 发送阻塞
**即使使用了 `context.WithTimeout`,问题依然存在!**
```go
// 主协程(控制器)
case <-ctxWithTimeout.Done():
logger.Log("超时了")
return temp // ⚠️ 返回了,不再从 channel 读取数据
// 子协程(服务层)
ch <- GoodsRes // ⚠️ 永久阻塞!因为没有接收者了
```
### 问题链条
```
1. 主协程 5 秒超时
2. 主协程返回,不再从 channel 读取
3. 子协程还在运行(可能需要 10 秒才能完成)
4. 子协程执行完毕,尝试 ch <- GoodsRes
5. ⚠️ channel 没有接收者,发送操作永久阻塞
6. 协程无法退出,持有 Redis/MongoDB 连接
7. 连接池逐渐耗尽
8. 后续请求无法获取连接 → 全部超时
```
## ✅ 解决方案
### 修复 1:发送 channel 时使用 select
**文件**: `service/service_ly.go`, `service/service_zy.go`
**修改前(会阻塞)**
```go
func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, goodsIds []string, ch chan sync.Map) {
// ... 处理逻辑
ch <- GoodsRes // ⚠️ 如果主协程已经返回,这里会永久阻塞
}
```
**修改后(不会阻塞)**
```go
func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, goodsIds []string, ch chan sync.Map) {
// ... 处理逻辑
// ⭐ 关键修复:使用 select 检查 context
select {
case <-ctx.Done():
logger.Log("发送结果前context已取消,直接返回", "sku", 1)
return // 不发送数据,直接返回
case ch <- GoodsRes:
// 成功发送
}
}
```
**为什么这样就不会阻塞?**
- 如果 channel 有接收者:`ch <- GoodsRes` 成功发送
- 如果主协程已经超时返回:`ctx.Done()` 会触发,直接 return
- **不会永久等待**
### 修复 2:超时后继续清空 channel
**文件**: `controller/sku_controller.go`
**修改前(会导致发送者阻塞)**
```go
case <-ctxWithTimeout.Done():
logger.Log("超时了")
time.Sleep(100 * time.Millisecond)
return temp // ⚠️ 直接返回,不再读取 channel
```
**修改后(不会导致发送者阻塞)**
```go
case <-ctxWithTimeout.Done():
logger.Log("超时了")
// ⭐ 关键修复:继续从 channel 读取数据
// 避免子协程在发送时阻塞
cleanupTimeout := time.After(200 * time.Millisecond)
for {
select {
case GoodsRes, ok := <-ch:
if !ok {
return temp // channel 已关闭,所有协程都已退出
}
// 收集已经发送的数据
GoodsRes.Range(func(k, v interface{}) bool {
s, _ := k.(string)
temp[s] = v
return true
})
case <-cleanupTimeout:
// 清理超时,强制返回
logger.Log("清理超时,强制返回", "sku", 1)
return temp
}
}
```
**为什么需要继续读取?**
- 有些子协程可能已经执行完毕,正在发送数据
- 如果不读取,它们会阻塞在 `ch <- GoodsRes`
- 继续读取 200ms,给它们一个发送的机会
- 配合修复1,超过 200ms 的协程会因为 `ctx.Done()` 而直接返回
## 🎯 两个修复的配合
```
场景1:子协程在 200ms 内完成
├─ 主协程:继续读取 channel
├─ 子协程:成功发送 ch <- GoodsRes
└─ 结果:✅ 数据被收集,协程正常退出
场景2:子协程超过 200ms 才完成
├─ 主协程:200ms 后强制返回
├─ 子协程:尝试发送,但 ctx.Done() 触发
└─ 结果:✅ 协程直接返回,不会阻塞
场景3:子协程在处理过程中检测到取消
├─ 主协程:超时,发送取消信号
├─ 子协程:循环中检测到 ctx.Done()
└─ 结果:✅ 协程立即退出,不会发送数据
```
## 📊 修复前后对比
| 场景 | 修复前 | 修复后 |
|------|--------|--------|
| 正常请求 | ✅ 正常 | ✅ 正常 |
| 第1次超时 | ⚠️ 留下僵尸协程 | ✅ 协程正常退出 |
| 第2次超时 | ⚠️ 留下更多僵尸协程 | ✅ 协程正常退出 |
| 第N次超时 | 🔴 连接池耗尽 | ✅ 协程正常退出 |
| 后续请求 | 🔴 全部超时 | ✅ 正常处理 |
| 需要重启 | 🔴 是 | ✅ 否 |
## 🔑 关键要点
### 1. Channel 发送是阻塞操作
```go
ch <- data // 如果没有接收者,会永久阻塞
```
### 2. 使用 select 可以避免阻塞
```go
select {
case <-ctx.Done():
return // 被取消,不发送
case ch <- data:
// 成功发送
}
```
### 3. 超时后不要立即返回
```go
// ❌ 错误
case <-timeout:
return
// ✅ 正确
case <-timeout:
// 继续读取 channel 一段时间
for { ... }
```
## 📝 修改清单
1.`service/service_ly.go` - 第 328-333 行
- 发送 channel 时使用 select
2.`service/service_zy.go` - 第 60-66 行,357-365 行
- 循环中检测到取消时不发送数据
- 发送 channel 时使用 select
3.`controller/sku_controller.go` - 第 168-195 行
- 超时后继续清空 channel
## 🧪 验证方法
### 1. 模拟慢查询
```bash
# 在 Redis 或 MongoDB 中模拟慢查询
# 或者在代码中添加 time.Sleep(10 * time.Second)
```
### 2. 观察日志
```
正常情况:
- "协程整体处理超时,所有子协程已收到取消信号"
- "发送结果前context已取消,直接返回"
- "清理超时,强制返回"
异常情况(修复前):
- "协程整体处理超时"
- 之后没有任何日志(协程阻塞了)
```
### 3. 监控连接数
```bash
# 查看 Redis 连接数
redis-cli CLIENT LIST | wc -l
# 查看 MongoDB 连接数
mongo --eval "db.serverStatus().connections"
```
**修复前**:连接数持续增长,最终耗尽
**修复后**:连接数稳定,正常回收
### 4. 压力测试
```bash
# 并发发送 100 个请求
ab -n 100 -c 10 "http://localhost:8080/api/sku?goods_id=xxx"
# 观察是否有请求超时
# 修复前:后续请求会全部超时
# 修复后:每个请求独立,不会相互影响
```
## 💡 经验总结
### 这是一个经典的 Go 并发陷阱
1. **Channel 不是万能的**
- 发送操作会阻塞
- 必须确保有接收者
2. **Context 取消不会自动清理 Channel**
- Context 只是一个信号
- 需要代码主动检查并响应
3. **超时处理要考虑清理工作**
- 不能一超时就返回
- 要给协程一个退出的机会
4. **Select 是避免阻塞的利器**
```go
select {
case <-ctx.Done():
return
case ch <- data:
}
```
### 适用场景
这个修复方案适用于所有类似的场景:
- 使用 channel 在协程间传递数据
- 主协程可能提前退出(超时、取消等)
- 需要避免协程泄漏
## 🎉 总结
**问题本质**:Channel 发送阻塞导致协程泄漏
**解决方案**
1. 发送时使用 select 检查 context
2. 超时后继续清空 channel
**效果**
- ✅ 协程能够正常退出
- ✅ 连接正常回收
- ✅ 后续请求不受影响
- ✅ 服务可以长期稳定运行
这是一个**必须掌握**的 Go 并发编程最佳实践!
# 协程超时问题优化说明
# 协程超时问题优化说明
......@@ -5,7 +5,33 @@
### 原始问题
在原有代码中,当一个请求超时后,后续所有请求都会超时,只有重启服务才能恢复正常。
### 问题根源
### 问题根源(关键发现!)
**最核心的问题:Channel 阻塞导致协程泄漏**
当超时发生时:
```go
case <-timeout:
logger.Log("协程整体处理超时", "sku", 1)
return temp // ⚠️ 主协程返回,不再从 channel 读取数据
```
但是子协程还在尝试发送数据:
```go
ch <- GoodsRes // ⚠️ 永久阻塞!因为没有接收者了
```
**问题链条**
1. 主协程超时返回 → 不再从 channel 读取
2. 子协程执行完毕,尝试 `ch <- GoodsRes`
3. **channel 没有接收者,发送操作永久阻塞**
4. 协程无法退出,持有 Redis/MongoDB 连接
5. 连接池逐渐耗尽
6. 后续请求无法获取连接 → 全部超时
**这就是为什么即使使用了 context.WithTimeout,问题依然存在!**
### 其他问题
1. **gin.Context 在协程中的并发问题**
- `gin.Context` 是和 HTTP 请求绑定的,生命周期只在当前请求处理期间有效
......@@ -95,6 +121,7 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
- 使用标准库的 `context.Context` 替代 `gin.Context`
- 使用 `RequestParams` 传递参数
- 在方法开始和循环中检查 context 是否已取消
- **关键:在发送 channel 时使用 select,避免阻塞**
```go
// 方法开始时检查
......@@ -110,14 +137,28 @@ for goodsId, skuStr := range skuArr {
select {
case <-ctx.Done():
logger.Log("LyGoodsDetail: 处理过程中context被取消", "sku", 1)
ch <- GoodsRes
// 不要发送数据,直接返回
return
default:
}
// ... 处理逻辑
}
// ⭐ 关键修复:发送结果时也要检查 context
select {
case <-ctx.Done():
logger.Log("LyGoodsDetail: 发送结果前context已取消,直接返回", "sku", 1)
return
case ch <- GoodsRes:
// 成功发送
}
```
**为什么这个修改至关重要?**
- 如果主协程已经超时返回,不再从 channel 读取
- 普通的 `ch <- GoodsRes` 会永久阻塞
- 使用 `select` 可以在 context 取消时立即返回,避免阻塞
#### 3. 修改控制器层
**文件**: `controller/sku_controller.go`
......@@ -147,9 +188,26 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
case <-ctxWithTimeout.Done():
// context 超时或被取消
logger.Log("协程整体处理超时,所有子协程已收到取消信号", "sku", 1)
// 等待一小段时间让协程清理资源
time.Sleep(100 * time.Millisecond)
return temp
// ⭐ 关键修复:继续从 channel 读取已经发送的数据,避免协程阻塞
cleanupTimeout := time.After(200 * time.Millisecond)
for {
select {
case GoodsRes, ok := <-ch:
if !ok {
return temp // channel 已关闭
}
// 收集已经发送的数据
GoodsRes.Range(func(k, v interface{}) bool {
s, _ := k.(string)
temp[s] = v
return true
})
case <-cleanupTimeout:
// 清理超时,强制返回
return temp
}
}
}
}
}
......@@ -201,7 +259,26 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
- 在耗时操作前检查
- 收到取消信号后立即返回
4. **确保资源能够正确释放**
4. **⭐ 向 channel 发送数据时必须使用 select(最关键!)**
```go
// ❌ 错误:可能永久阻塞
ch <- result
// ✅ 正确:可以被 context 取消
select {
case <-ctx.Done():
return
case ch <- result:
// 成功发送
}
```
5. **超时后继续清空 channel**
- 主协程超时后,不要立即返回
- 继续从 channel 读取数据,避免发送者阻塞
- 设置一个短暂的清理超时
6. **确保资源能够正确释放**
- 使用 defer 关闭连接
- 超时时等待一小段时间让协程清理
......@@ -219,6 +296,23 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
- 必须有机制取消超时的协程
- 否则会导致资源泄漏
4. **⭐ 不要直接向 channel 发送数据(最容易犯的错误!)**
```go
// ❌ 危险:如果接收者已经退出,会永久阻塞
ch <- result
// ✅ 安全:可以被 context 取消
select {
case <-ctx.Done():
return
case ch <- result:
}
```
5. **不要在超时后立即返回**
- 必须继续清空 channel
- 否则发送者会阻塞
## 修改文件清单
1. ✅ `service/request_params.go` - 新建,定义请求参数结构体
......
......@@ -80,7 +80,7 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
select {
case <-ctx.Done():
logger.Log("LyGoodsDetail: 处理过程中context被取消", "sku", 1)
ch <- GoodsRes
// 不发送不完整的数据,直接返回让协程尽快退出
return
default:
}
......@@ -323,11 +323,13 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
//(*goodsRes)[goodsId] = A
}
// 发送结果时也要检查 context,避免在超时后阻塞
select {
case <-ctx.Done():
logger.Log("LyGoodsDetail: 发送结果前context已取消,直接返回", "sku", 1)
return
default:
ch <- GoodsRes
case ch <- GoodsRes:
// 成功发送
}
}
......
......@@ -60,7 +60,7 @@ func (qs *ZiyingService) ZyGoodsDetail(ctx context.Context, params RequestParams
select {
case <-ctx.Done():
logger.Log("ZyGoodsDetail: 处理过程中context被取消", "sku", 1)
ch <- GoodsRes
// 不要在这里发送,直接返回
return
default:
}
......@@ -354,5 +354,12 @@ func (qs *ZiyingService) ZyGoodsDetail(ctx context.Context, params RequestParams
//最后写入sync map
(GoodsRes).Store(goodsId, A)
}
ch <- GoodsRes
// 发送结果时也要检查 context,避免在超时后阻塞
select {
case <-ctx.Done():
logger.Log("ZyGoodsDetail: 发送结果前context已取消,直接返回", "sku", 1)
return
case ch <- GoodsRes:
// 成功发送
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment