Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
黄成意
/
go_sku_server
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
720fb45b
authored
Oct 27, 2025
by
杨树贤
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
优化版本的
parent
315863bc
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
422 additions
and
15 deletions
controller/sku_controller.go
docs/Channel阻塞问题-关键修复.md
docs/协程超时优化说明.md
service/service_ly.go
service/service_zy.go
controller/sku_controller.go
View file @
720fb45b
...
@@ -168,9 +168,29 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
...
@@ -168,9 +168,29 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
case
<-
ctxWithTimeout
.
Done
()
:
case
<-
ctxWithTimeout
.
Done
()
:
// context 超时或被取消
// context 超时或被取消
logger
.
Log
(
"协程整体处理超时,所有子协程已收到取消信号"
,
"sku"
,
1
)
logger
.
Log
(
"协程整体处理超时,所有子协程已收到取消信号"
,
"sku"
,
1
)
// 等待一小段时间让协程清理资源
time
.
Sleep
(
100
*
time
.
Millisecond
)
// 关键:继续从 channel 读取已经发送的数据,避免协程阻塞
return
temp
// 超时,返回已经收到的部分数据
// 设置一个短暂的清理超时
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
}
}
}
}
}
}
}
}
...
...
docs/Channel阻塞问题-关键修复.md
0 → 100644
View file @
720fb45b
# 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 并发编程最佳实践!
docs/协程超时优化说明.md
View file @
720fb45b
# 协程超
时问题优化说明
# 协程超
时问题优化说明
...
@@ -5,7 +5,33 @@
...
@@ -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 在协程中的并发问题**
1.
**gin.Context 在协程中的并发问题**
-
`gin.Context`
是和 HTTP 请求绑定的,生命周期只在当前请求处理期间有效
-
`gin.Context`
是和 HTTP 请求绑定的,生命周期只在当前请求处理期间有效
...
@@ -95,6 +121,7 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
...
@@ -95,6 +121,7 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
- 使用标准库的 `context.Context` 替代 `gin.Context`
- 使用标准库的 `context.Context` 替代 `gin.Context`
- 使用 `RequestParams` 传递参数
- 使用 `RequestParams` 传递参数
- 在方法开始和循环中检查 context 是否已取消
- 在方法开始和循环中检查 context 是否已取消
- **关键:在发送 channel 时使用 select,避免阻塞**
```
go
```
go
// 方法开始时检查
// 方法开始时检查
...
@@ -110,14 +137,28 @@ for goodsId, skuStr := range skuArr {
...
@@ -110,14 +137,28 @@ for goodsId, skuStr := range skuArr {
select {
select {
case <-ctx.Done():
case <-ctx.Done():
logger.Log("LyGoodsDetail: 处理过程中context被取消", "sku", 1)
logger.Log("LyGoodsDetail: 处理过程中context被取消", "sku", 1)
ch <- GoodsRes
// 不要发送数据,直接返回
return
return
default:
default:
}
}
// ... 处理逻辑
// ... 处理逻辑
}
}
// ⭐ 关键修复:发送结果时也要检查 context
select {
case <-ctx.Done():
logger.Log("LyGoodsDetail: 发送结果前context已取消,直接返回", "sku", 1)
return
case ch <- GoodsRes:
// 成功发送
}
```
```
**为什么这个修改至关重要?**
- 如果主协程已经超时返回,不再从 channel 读取
- 普通的 `ch <- GoodsRes` 会永久阻塞
- 使用 `select` 可以在 context 取消时立即返回,避免阻塞
#### 3. 修改控制器层
#### 3. 修改控制器层
**文件**: `controller/sku_controller.go`
**文件**: `controller/sku_controller.go`
...
@@ -147,9 +188,26 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
...
@@ -147,9 +188,26 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
case <-ctxWithTimeout.Done():
case <-ctxWithTimeout.Done():
// context 超时或被取消
// context 超时或被取消
logger.Log("协程整体处理超时,所有子协程已收到取消信号", "sku", 1)
logger.Log("协程整体处理超时,所有子协程已收到取消信号", "sku", 1)
// 等待一小段时间让协程清理资源
time.Sleep(100 * time.Millisecond)
// ⭐ 关键修复:继续从 channel 读取已经发送的数据,避免协程阻塞
return temp
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{} {
...
@@ -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 关闭连接
- 使用 defer 关闭连接
- 超时时等待一小段时间让协程清理
- 超时时等待一小段时间让协程清理
...
@@ -219,6 +296,23 @@ func CommonController(ctx *gin.Context) map[string]interface{} {
...
@@ -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` - 新建,定义请求参数结构体
1. ✅ `service/request_params.go` - 新建,定义请求参数结构体
...
...
service/service_ly.go
View file @
720fb45b
...
@@ -80,7 +80,7 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
...
@@ -80,7 +80,7 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
select
{
select
{
case
<-
ctx
.
Done
()
:
case
<-
ctx
.
Done
()
:
logger
.
Log
(
"LyGoodsDetail: 处理过程中context被取消"
,
"sku"
,
1
)
logger
.
Log
(
"LyGoodsDetail: 处理过程中context被取消"
,
"sku"
,
1
)
ch
<-
GoodsRes
// 不发送不完整的数据,直接返回让协程尽快退出
return
return
default
:
default
:
}
}
...
@@ -323,11 +323,13 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
...
@@ -323,11 +323,13 @@ func (ls *LyService) LyGoodsDetail(ctx context.Context, params RequestParams, go
//(*goodsRes)[goodsId] = A
//(*goodsRes)[goodsId] = A
}
}
// 发送结果时也要检查 context,避免在超时后阻塞
select
{
select
{
case
<-
ctx
.
Done
()
:
case
<-
ctx
.
Done
()
:
logger
.
Log
(
"LyGoodsDetail: 发送结果前context已取消,直接返回"
,
"sku"
,
1
)
return
return
default
:
case
ch
<-
GoodsRes
:
ch
<-
GoodsRes
// 成功发送
}
}
}
}
...
...
service/service_zy.go
View file @
720fb45b
...
@@ -60,7 +60,7 @@ func (qs *ZiyingService) ZyGoodsDetail(ctx context.Context, params RequestParams
...
@@ -60,7 +60,7 @@ func (qs *ZiyingService) ZyGoodsDetail(ctx context.Context, params RequestParams
select
{
select
{
case
<-
ctx
.
Done
()
:
case
<-
ctx
.
Done
()
:
logger
.
Log
(
"ZyGoodsDetail: 处理过程中context被取消"
,
"sku"
,
1
)
logger
.
Log
(
"ZyGoodsDetail: 处理过程中context被取消"
,
"sku"
,
1
)
ch
<-
GoodsRes
// 不要在这里发送,直接返回
return
return
default
:
default
:
}
}
...
@@ -354,5 +354,12 @@ func (qs *ZiyingService) ZyGoodsDetail(ctx context.Context, params RequestParams
...
@@ -354,5 +354,12 @@ func (qs *ZiyingService) ZyGoodsDetail(ctx context.Context, params RequestParams
//最后写入sync map
//最后写入sync map
(
GoodsRes
)
.
Store
(
goodsId
,
A
)
(
GoodsRes
)
.
Store
(
goodsId
,
A
)
}
}
ch
<-
GoodsRes
// 发送结果时也要检查 context,避免在超时后阻塞
select
{
case
<-
ctx
.
Done
()
:
logger
.
Log
(
"ZyGoodsDetail: 发送结果前context已取消,直接返回"
,
"sku"
,
1
)
return
case
ch
<-
GoodsRes
:
// 成功发送
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment