fiber 框架需手动管理多数据库切换,禁用全局变量以防竞态,应通过 context.locals 或依赖注入绑定 db 实例;gorm 多数据源下事务不可跨库,须人工约束或采用最终一致性;读写分离需按一致性要求路由,连接超时应分设。

Fiber 是 Go 语言的轻量级 Web 框架,本身不内置 ORM 或数据库连接池管理,所谓“多数据库数据源切换”,实际是开发者在应用层对多个 *sql.DB 实例做路由控制。它不像 Laravel 的 DB::connection('mysql2') 那样有语法糖,也不同于 Spring Boot 的 @Transactional(transactionManager = "tm2") 可声明式绑定——一切得靠手动传参、上下文携带或依赖注入控制。
为什么不能直接用全局变量存多个 *sql.DB?
能存,但危险:*sql.DB 是并发安全的,但如果你在中间件里动态修改某个全局变量指向的 DB 实例(比如根据请求 header 切换读库),就会引发竞态:两个 goroutine 同时写这个变量,一个刚赋值完,另一个就覆盖了。更糟的是,这种 bug 在压测前几乎不暴露。
正确做法是把 DB 实例作为依赖显式注入,或通过 fiber.Ctx.Locals 绑定到当前请求生命周期:
- 避免使用包级变量(如
var db *sql.DB)做运行时切换 - 每个数据源初始化独立的
*sql.DB,并调用SetMaxOpenConns/SetMaxIdleConns单独调优 - 在中间件中根据规则(如 path 前缀、JWT claim、query 参数)选择对应 DB,并存入
c.Locals("db") - 后续 handler 从
c.Locals("db")取,类型断言为*sql.DB
gorm 多数据源下如何避免事务跨库?
用 gorm 时,很容易写出这样的代码:
tx := db1.Begin()
tx.Create(&user)
tx.Exec("INSERT INTO logs ...") // 错!logs 表在 db2 上
tx.Commit()
这会导致事务只在 db1 生效,db2 的语句根本没进事务。GORM 不支持跨 *gorm.DB 实例的分布式事务(也没打算支持)。
关键约束必须人工守住:
- 一个事务只操作来自同一个
*gorm.DB实例的表 - 如果业务必须跨库写(如主库写订单 + 日志库写审计),改用最终一致性:主库成功后发消息,由消费者写日志库
- 不要复用
db1.Session(&gorm.Session{NewDB: true})去“伪造”另一个库的 session —— 它只是新建 session,底层还是用原*sql.DB
怎么让 repository 层自动感知当前数据源?
硬编码 repo.UserRepo{DB: db1} 或每次传参都太啰嗦。推荐用函数选项模式封装构造逻辑:
type RepoOption func(*repo.Config)
func WithDB(db *sql.DB) RepoOption {
return func(c *repo.Config) { c.DB = db }
}
func NewUserRepo(opts ...RepoOption) *UserRepo {
cfg := &repo.Config{}
for _, opt := range opts {
opt(cfg)
}
return &UserRepo{db: cfg.DB}
}
然后在 handler 中按需构造:
func handler(c *fiber.Ctx) error {
db := c.Locals("db").(*sql.DB)
repo := repo.NewUserRepo(repo.WithDB(db))
return repo.FindByID(c.Params("id"))
}
这样既解耦,又不会因依赖注入容器缺失而卡住——Fiber 项目多数不引入 DI 框架。
读写分离时,SELECT 一定走从库吗?
不一定。常见错误是:所有 GET /api/users 请求都无脑走从库,结果用户刚创建完立刻查不到(主从延迟)。真正可控的做法是:
- 普通列表页、后台报表等容忍延迟的场景,显式调用
slaveDB - 用户刚提交的资源(如
POST /orders后立即GET /orders/123),应强制走主库 —— 可加 query 参数?consistency=strong触发路由逻辑 - 避免在事务内执行
SELECT后再UPDATE:哪怕用了主库,若中间被其他事务改了数据,仍可能产生幻读;此时应加SELECT ... FOR UPDATE
最易被忽略的一点:连接字符串里的 readTimeout 和 writeTimeout 应分开配置。从库查询慢不影响主库写入超时,否则一个慢从库会拖垮整个写链路。











