redis避免耗尽连接池相关泄露的问题

Redis避免耗尽连接池以及相关泄露的问题

问题背景

在编写《蒙东电网》接口服务时,需要频繁调用redis中的内容,造成了大量的redis的连接,但是连接没有及时关闭,最后导致蒙东电网的接口服务在单一设备的情况下运行大约一周左右便会出现无法继续上报数据的问题,经过排查发现问题在于redis的连接池的配置问题,导致连接数量过多后没能及时关闭连接,最后造成新的获取缓存数据的服务无法建立连接,从而导致无法上报相关数据。

解决方式

主要分为配置文件的优化,程序代码的连接关闭,重试机制以及超时管理。

一、配置文件修改(config.yaml)

1. Redis 连接池配置优化

1
2
3
# 连接池配置(优化:降低连接池大小,避免与sagooiot-professional竞争资源)
poolSize: 50 # 连接池容量(优化:从500降低到50)
minIdle: 10 # 最小空闲连接数(优化:从50降低到10)

修改说明:

  • poolSize: 500 → 50(降低 90%)
    • 限制最大连接数,避免耗尽 Redis 连接
    • 两个服务共享 Redis,总和不超过服务器限制
  • minIdle: 50 → 10(降低 80%)
    • 减少常驻空闲连接,降低内存占用
    • 仍保留最小空闲连接,保证响应速度

2. MySQL 连接池配置优化

1
2
3
maxIdle: 20 #连接池最大闲置的连接数(优化:从50降低到20,避免资源竞争)
maxOpen: 100 #连接池最大打开的连接数(优化:从1000降低到100,避免耗尽数据库连接)
maxLifetime: 3600 #(单位秒)连接对象可重复使用的时间长度(优化:从120秒增加到3600秒,减少频繁重建连接)

修改说明:

  • maxOpen: 1000 → 100(降低 90%)
  • maxIdle: 50 → 20(降低 60%)
  • maxLifetime: 120 → 3600(增加 30 倍)
    • 减少频繁重建连接,降低连接池压力

二、代码层面的防泄漏机制

1. 超时控制机制

realtime_pipeline.go 中添加了超时控制:

1
2
3
4
func cacheAppendToday(ctx context.Context, domain Domain, device string, v any) error {
// 添加超时控制,避免长时间阻塞
opCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

作用:

  • 每个 Redis 操作最多 10 秒超时
  • 超时后自动取消,连接归还连接池
  • 防止因网络问题导致连接长时间占用

2. 重试机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 添加重试机制,最多重试3次
maxRetries := 3
var lastErr error
for i := 0; i < maxRetries; i++ {
// 检查上下文是否已取消
if opCtx.Err() != nil {
return fmt.Errorf("操作超时: %w", opCtx.Err())
}

redis := g.Redis()

// LPUSH + LTRIM 裁剪(使用超时上下文)
if _, err := redis.LPush(opCtx, key, b); err != nil {
// 检查是否超时
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("Redis LPUSH超时: %w", err)
}
lastErr = err
g.Log().Warningf(opCtx, "Redis LPUSH失败(尝试%d/%d),键: %s, 错误: %v", i+1, maxRetries, key, err)
if i < maxRetries-1 {
time.Sleep(time.Duration(i+1) * 100 * time.Millisecond) // 递增等待时间
}
continue
}
if _, err := redis.Do(opCtx, "LTRIM", key, 0, max-1); err != nil {
// 检查是否超时
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("Redis LTRIM超时: %w", err)
}
lastErr = err
g.Log().Warningf(opCtx, "Redis LTRIM失败(尝试%d/%d),键: %s, 错误: %v", i+1, maxRetries, key, err)
if i < maxRetries-1 {
time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
}
continue
}
if _, err := redis.Do(opCtx, "EXPIRE", key, int64(ttlH*3600)); err != nil {
// 检查是否超时
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("Redis EXPIRE超时: %w", err)
}
lastErr = err
g.Log().Warningf(opCtx, "Redis EXPIRE失败(尝试%d/%d),键: %s, 错误: %v", i+1, maxRetries, key, err)
if i < maxRetries-1 {
time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
}
continue
}
// 所有操作成功
return nil
}

// 所有重试都失败
return fmt.Errorf("Redis写入失败,已重试%d次,最后错误: %w", maxRetries, lastErr)

作用:

  • 失败时最多重试 3 次
  • 每次重试前检查超时,避免无效重试
  • 重试间隔递增,避免瞬时压力
  • 失败后连接正常归还,不会泄漏

三、框架层面的连接管理(GoFrame)

1. 连接自动归还机制

代码中使用 g.Redis() 获取连接:

1
redis := g.Redis()

GoFrame 的连接管理机制:

  • 自动连接池:g.Redis() 返回的是连接池客户端,不是单个连接
  • 自动归还:操作完成后,连接自动归还到连接池
  • 连接复用:多个操作共享连接池中的连接

2. 连接池生命周期管理

GoFrame 框架会自动:

  • 启动时创建连接池(懒加载)
  • 根据 poolSizeminIdle 管理连接数量
  • 空闲连接自动清理(超过 idleTimeout 后关闭)
  • 连接异常时自动重建

四、防止连接泄漏的机制总结

1. 配置层面

配置项 修改前 修改后 防泄漏作用
poolSize 500 50 限制最大连接数,防止无限制增长
minIdle 50 10 减少常驻连接,降低泄漏影响
maxOpen 1000 100 限制数据库连接数
maxLifetime 120秒 3600秒 减少频繁重建,降低连接池压力

2. 代码层面

机制 实现位置 防泄漏作用
超时控制 context.WithTimeout(ctx, 10*time.Second) 防止连接长时间占用
上下文取消 opCtx.Err() != nil 检查 超时后立即释放资源
错误处理 重试机制中的错误检查 确保异常时连接正常归还
框架管理 g.Redis() 自动管理 GoFrame 自动处理连接生命周期

3. 连接泄漏防护流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 获取连接
redis := g.Redis() // 从连接池获取(不创建新连接)

2. 设置超时
opCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel() // 确保超时后释放资源

3. 执行操作
redis.LPush(opCtx, key, b) // 使用超时上下文

4. 自动归还
- 操作成功:连接自动归还到连接池
- 操作超时:context 取消,连接自动归还
- 操作失败:重试或返回错误,连接自动归还

5. 连接池管理
- 空闲连接超过 minIdle:保留 minIdle 个
- 连接数超过 poolSize:等待或拒绝新请求
- 连接异常:自动重建

五、为什么这样配置能防止泄漏?

1. 限制连接数上限

  • poolSize: 50 限制最大连接数
  • 即使有泄漏,最多泄漏 50 个连接
  • 不会无限增长导致服务器崩溃

2. 超时机制强制释放

  • 10 秒超时确保连接不会永久占用
  • 即使代码有 bug,超时后也会释放

3. 框架自动管理

  • GoFrame 自动处理连接生命周期
  • 不需要手动 Close(),减少人为错误

4. 减少空闲连接

  • minIdle: 10 减少常驻连接
  • 即使有泄漏,影响也较小

六、监控建议

可以添加以下监控来验证配置是否生效:

1
2
3
4
5
# 监控 Redis 连接数
redis-cli INFO clients | grep connected_clients

# 监控 MySQL 连接数
mysql -u root -p -e "SHOW STATUS LIKE 'Threads_connected';"

如果连接数稳定在配置范围内,说明配置生效,连接没有泄漏。


总结

主要修改:

  1. 配置文件:降低 poolSizeminIdle
  2. 代码层面:添加超时控制和重试机制
  3. 框架层面:依赖 GoFrame 的自动连接管理

这些改动共同确保:

  • 连接数不会超过限制
  • 超时后自动释放
  • 异常情况下连接正常归还
  • 两个服务共享资源时不会互相影响

如需进一步优化,可添加连接池监控和告警机制。