安全初始化 sql.db 需为每个租户创建独立实例并设限:db.setmaxopenconns(5)、setmaxidleconns(2),存入 map[string]*sql.db 按 tenant_id 缓存,首次访问时初始化,通过 context 传递避免泄漏。

sql.DB 不是单个连接,而是连接池抽象;GORM 是 ORM 层,不替代连接管理;mongo.Client 和 redis.Client 同理——它们各自维护独立连接池。直接复用全局实例或忽略初始化校验,90% 的“连不上”“超时”“OOM”都源于此。
怎么安全初始化 sql.DB 并避免租户间连接泄漏
多租户场景下,不能只建一个 *sql.DB 实例然后到处传。每个租户必须有自己隔离的连接池,否则 A 租户慢查询会耗尽连接,B 租户直接卡在 sql.ErrConnDone。
- 用
map[string]*sql.DB缓存租户级*sql.DB,key 是tenant_id - 首次访问某租户时才调
sql.Open("pgx", dsn),别在启动时全连一遍 - 显式设限:
db.SetMaxOpenConns(5)和db.SetMaxIdleConns(2),不设就是默认 0(无上限) - 把实例存进请求上下文:
c.Locals("tenant_db", db),后续 handler 用c.Locals("tenant_db").(*sql.DB)拿,别再查 map
GORM 怎么和 Fiber 配合又不污染中间件逻辑
GORM 本身不绑定框架,但容易误用成“全局 DB 单例”。它应该只负责模型映射和查询构造,连接池仍由底层 *sql.DB 管理。
- 不要写
app.Use(DBMiddleware(db))把整个*gorm.DB塞进所有请求——租户隔离就破了 - 正确做法:中间件里根据租户动态生成
*gorm.DB,例如gorm.Open(gormAdapter, &gorm.Config{})+db.Session(&session) - 模型定义里慎用
gorm.Model,它自带ID、CreatedAt等字段,和租户表前缀或分库策略可能冲突 - 迁移操作(
AutoMigrate)必须按租户执行,不能对全局*gorm.DB一把梭
mongo.Client 和 redis.Client 初始化时最常漏的三件事
这两个客户端都自带连接池,但初始化不校验、不设参、不传 ctx,上线后第一波流量就报错。
-
mongo.Client必须调client.Connect(ctx)+client.Ping(ctx, readpref.Primary()),否则第一次Find()才连,失败难定位 -
redis.Client的Options.PoolSize默认是 10,中高并发服务建议设为 20~50;Password字段不能留空字符串,没密码也要显式填"" - 所有
Find()返回的*mongo.Cursor必须defer cursor.Close(ctx),漏掉就会 goroutine 泄漏,压测几分钟就 OOM
Handler 里读写数据库/缓存的最小安全单元写法
别在 handler 里 new client、open db、defer close——这些是应用生命周期行为,不是请求级行为。
- DB 操作:从
c.Locals("tenant_db")拿*sql.DB,用db.QueryRowContext(c.Context(), ...),别用db.QueryRow(...)(丢失 cancel 控制) - Redis 写缓存:
client.Set(c.Context(), key, value, ttl),value 是 string/int/struct,struct 要可 JSON 序列化 - Redis 读缓存:
val, err := client.Get(c.Context(), key).Result(),判断不存在必须用errors.Is(err, redis.Nil),不是err == nil - Mongo 查询:
cursor, _ := collection.Find(c.Context(), filter),后面紧跟defer cursor.Close(c.Context())











