问题修复: - 修复 TimeoutMiddleware 死锁问题(嵌套锁调用) - 修复竞态条件(responseSent 标志确保只发送一次响应) - 基准测试超时从 5ms 改为 100ms 避免 race 检测不稳定 文档更新: - 添加中间件并发测试要点(testing_strategy_v1.md) - 添加 TimeoutMiddleware 并发安全经验(project_experience_summary.md) - 更新测试覆盖率报告 - 新建项目状态报告
9.7 KiB
Supply API 项目经验总结
本文档总结项目实施过程中的关键经验教训
一、设计阶段常见问题
1.1 跨文档命名不一致
问题描述:
在代码审查中发现多处字段命名不一致,如 ClientIP vs SourceIP,导致类型转换错误。
受影响的文件:
auth.go使用ClientIPaudit_event.go使用SourceIP
修复方案:
统一使用 SourceIP,更新所有引用。
经验教训:
- 建立跨模块字段命名标准文档
- Code Review 时重点检查命名一致性
- 使用 linter 检测不一致的字段名
1.2 接口定义与实现不匹配
问题描述: 领域层定义的 Store 接口缺少乐观锁参数,但实现层已支持。
示例:
// 接口定义(缺少版本控制)
type SettlementStore interface {
Update(ctx context.Context, s *Settlement) error
}
// 实现(已支持乐观锁)
func (r *SettlementRepository) Update(ctx context.Context, pkg *Settlement, expectedVersion int) error
修复方案:
同步更新接口定义,添加 expectedVersion 参数。
经验教训:
- 接口定义必须与实现保持同步
- 大型重构前先梳理接口依赖
- 使用接口适配器模式桥接新旧实现
1.3 缓存与吊销机制矛盾
问题描述: Token 缓存在有效期内无法及时吊销。
修复方案:
- 缓存 TTL 设置较短(10秒)
- 吊销时主动失效缓存
- 后端状态变更触发缓存刷新
经验教训:
- 缓存策略必须考虑吊销场景
- 主动失效优于被动过期
二、代码实现常见问题
2.1 重复代码
问题描述:
main.go 中存在与 healthcheck.go 重复的健康检查处理函数。
修复前:
// main.go 中的 inline handler
mux.HandleFunc("/actuator/health", handleHealthCheck(db, redisCache))
mux.HandleFunc("/actuator/health/live", handleLiveness)
mux.HandleFunc("/actuator/health/ready", handleReadiness)
// healthcheck.go 中已有的完整实现
type HealthHandler struct {
healthChecker *DefaultHealthChecker
readinessChecks []HealthChecker
livenessChecks []HealthChecker
}
修复后:
// 统一使用 HealthHandler
healthHandler := httpapi.NewHealthHandlerWithDefaults(dbHealthCheck, redisHealthCheck)
mux.HandleFunc("/actuator/health", healthHandler.ServeHealth)
经验教训:
- 优先使用已有的通用组件
- 避免在 main.go 中直接实现业务逻辑
- 定期清理不再使用的 inline handlers
2.2 结构化日志缺失
问题描述:
Logging 中间件使用标准库 log.Printf 而非结构化日志。
修复前:
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path) // 非结构化
next.ServeHTTP(w, r)
})
}
修复后:
func Logging(next http.Handler, logger logging.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fields := map[string]interface{}{
"method": r.Method,
"path": r.URL.Path,
"trace_id": tc.TraceID,
}
logger.Info("HTTP request", fields)
next.ServeHTTP(w, r)
})
}
经验教训:
- 生产环境必须使用结构化日志
- 日志需包含:timestamp, level, trace_id, request_id, 业务字段
- 结构化日志便于查询和分析
2.3 未使用的导入和函数
问题描述: 代码变更后遗留未使用的导入和函数定义。
示例:
删除 inline handler 后未删除 encoding/json 导入。
经验教训:
- 使用
go vet和 IDE 检查未使用的导入 - 删除废弃代码而非注释
- 代码重构后立即清理相关引用
三、数据库设计问题
3.1 字段映射错误
问题描述:
Package Repository 中 SupplierID 重复映射到 supply_account_id 和 user_id。
修复前:
pkg.SupplierID, pkg.SupplierID, pkg.Platform, pkg.Model, // 错误:SupplierID 出现两次
修复后:
pkg.SupplierID, pkg.AccountID, pkg.Platform, pkg.Model, // 正确映射
经验教训:
- SQL 参数绑定时仔细核对字段顺序
- 使用结构体标签明确映射关系
- 编写数据库相关的单元测试
3.2 乐观锁与悲观锁选择
使用场景:
| 场景 | 锁策略 | 说明 |
|---|---|---|
| 结算状态更新 | 乐观锁 | 低频操作,冲突概率低 |
| 配额扣减 | 悲观锁 | 高并发,需要保证原子性 |
| 账户余额 | 悲观锁 | 财务敏感操作 |
经验教训:
- 根据业务场景选择合适的锁策略
- 乐观锁需处理
ErrConcurrencyConflict错误 - 悲观锁需考虑锁超时和死锁
四、中间件设计问题
4.1 Tracing 中间件缺失
问题描述: 未实现 W3C Trace Context 标准,无法进行分布式追踪。
修复方案:
// 解析 traceparent header
func ParseTraceParent(traceParent string) (*TraceContext, error) {
// 格式: 00-{trace-id}-{span-id}-{trace-flags}
// 长度: 55 字符
traceID := traceParent[3:35]
spanID := traceParent[36:52]
}
// 注入到 context
func TracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceParent := r.Header.Get("traceparent")
// 解析并注入 context
})
}
经验教训:
- 微服务必须实现分布式追踪
- 遵循 W3C Trace Context 标准
- trace_id 需要贯穿所有日志
4.2 TimeoutMiddleware 并发安全
问题描述: 超时中间件实现存在死锁和竞态条件,导致测试不稳定。
错误实现(死锁):
// 错误:主 goroutine 获取锁后等待 handler goroutine
mu.Lock()
go func() {
next.ServeHTTP(wrapped, r) // wrapped.WriteHeader() 尝试获取同一个锁
mu.Unlock() // 死锁!
}()
select {
case <-done:
return
case <-time.After(timeout):
mu.Lock() // 再次尝试获取锁 - 死锁!
// ...
}
错误实现(竞态):
// 错误:handler 和超时同时写入 ResponseWriter
go func() {
next.ServeHTTP(w, r) // 写入 200
close(handlerDone)
}()
select {
case <-handlerDone:
return
case <-time.After(timeout):
// handler 可能同时写入,造成竞态
http.Error(w, "timeout", 504)
}
正确实现:
func WithTimeoutMiddleware(next http.Handler, timeout time.Duration) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var mu sync.Mutex
responseSent := false
handlerDone := make(chan struct{})
go func() {
next.ServeHTTP(w, r)
close(handlerDone)
}()
select {
case <-handlerDone:
return
case <-time.After(timeout):
mu.Lock()
if !responseSent {
responseSent = true
mu.Unlock()
w.Header().Set("X-Timeout", "true")
http.Error(w, fmt.Sprintf("middleware timeout after %v", timeout), http.StatusGatewayTimeout)
return
}
mu.Unlock()
return
}
})
}
经验教训:
- 中间件的锁设计必须清晰:主 goroutine 和 handler goroutine 不能同时持有锁
- 使用
sync.Once或互斥锁 + 标志位确保响应只发送一次 - 超时设置必须足够长(建议 >100ms),避免在 race 检测下不稳定
- 基准测试和单元测试的超时设置需要合理匹配
- 测试覆盖率不等于测试质量:需要真正验证并发场景
五、测试问题
5.1 Mock 对象未正确覆盖所有方法
问题描述:
captureLogger 仅覆盖了 log() 方法,但测试调用的是 Info()、Debug() 等方法。
修复:
type captureLogger struct {
*jsonLogger
}
func (l *captureLogger) Info(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
l.log(LogLevelInfo, msg, f)
}
// 类似覆盖 Debug, Warn, Error, Fatal
经验教训:
- Go 嵌入式方法调用解析到被嵌入类型
- Mock 对象必须覆盖所有公共方法
- 编写测试后实际运行验证
六、项目管理问题
6.1 过期文件清理
问题描述: Git 仓库中遗留大量已删除但未清理的报告文件。
修复命令:
git rm $(git status --short | grep "^ D " | sed 's/^ D //')
经验教训:
- 定期清理已删除文件的 git 跟踪状态
- 报告文件使用归档目录而非版本控制
- CI/CD 流程自动清理过期文件
6.2 文档与代码不同步
问题描述: 代码变更后相关设计文档未同步更新。
经验教训:
- 文档更新纳入代码变更流程
- 使用文档即代码(Docs as Code)实践
- 自动化文档生成
七、关键设计决策记录
7.1 JWT Token 格式
- 算法:HS256(内部服务)/ RS256(跨服务)
- Claims:subject_id, role, scope, tenant_id, iat, exp
7.2 审计事件采样策略
- 成功率:1% 采样
- 失败率:100% 采样
7.3 健康检查路径
/actuator/health- 综合健康/actuator/health/live- 存活探针/actuator/health/ready- 就绪探针
八、改进建议
8.1 短期改进
- 完善单元测试覆盖率(当前 75% → 目标 85%)
- 补充集成测试
- 添加 API 文档(OpenAPI/Swagger)
8.2 中期改进
- 实现数据库连接池监控
- 添加 Redis 缓存命中率指标
- 完善错误码体系文档
8.3 长期改进
- 迁移到 gRPC
- 实现服务网格
- 添加 A/B 测试框架