主数据库宕机时备份库故障转移实现方案咨询(附Go代码)
数据库故障切换的Go代码方案咨询
现有系统流程代码
func main() { // conn 是数据库连接池 conn := getConnection() router := newRouter(conn) err := http.ListenAndServe(":3333", router) } func newConnection() *sql.DB { // 模拟具备故障转移功能的连接创建 // 如果数据库A宕机,则连接到数据库B db, err := sql.Open("postgres", "postgres://user:pass@localhost/bookstore") if err != nil { log.Fatal(err) } return db } func newRouter(db *sql.DB) http.Handler { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) // 在处理器内操作数据库 if err != nil { // 此处需要切换到备份连接 } }) return r }
该代码结构是"连接池封装"模式的修改版。主函数逻辑:从getConnection()获取数据库连接池,传入newRouter生成HTTP处理器,最后启动HTTP服务。
当前问题
当主数据库宕机时,处理器捕获错误后需要切换到备份数据库,但连接池由主函数传入,更新连接池需要修改处理器甚至重启HTTP服务,无法平滑切换。
临时解决方案
初始化时创建主、备两个连接池,同时传入路由器,主池故障时直接使用备池:
func main() { // conn 是主连接池 conn := getConnection() backupConn := getBackupConnection() router := newRouter(conn, backupConn) err := http.ListenAndServe(":3333", router) } func newRouter(db *sql.DB, backupDb *sql.DB) http.Handler { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) // 使用主池操作数据库 if err != nil { // 切换到备池操作 backupDb.Exec() } }) return r }
请问该方案是否属于最佳实践?还有其他可行的解决方案吗?
回答
临时方案的优缺点
优点
- 实现简单:不需要复杂的连接池管理逻辑,直接在错误分支切换到备池即可
- 无服务重启:备池提前初始化,故障时直接切换,无需重启HTTP服务
缺点
- 代码侵入性高:每个数据库操作的错误分支都要手动处理备池切换,容易遗漏
- 状态不同步:主备库的数据一致性需要依赖数据库自身的同步机制,代码层面无法保证
- 资源浪费:备池长期处于闲置状态,占用额外的数据库连接资源
- 无法自动切回:主库恢复后,需要手动修改代码或配置切回主池,无法自动感知
更优的解决方案
1. 封装连接池切换逻辑到自定义DB层
把主备切换的逻辑封装成一个自定义的DB结构体,对外暴露和*sql.DB一致的方法(如Exec、Query等),内部自动处理主备切换:
type FailoverDB struct { primary *sql.DB standby *sql.DB current *sql.DB mu sync.RWMutex } func NewFailoverDB(primaryDSN, standbyDSN string) (*FailoverDB, error) { primary, err := sql.Open("postgres", primaryDSN) if err != nil { return nil, err } standby, err := sql.Open("postgres", standbyDSN) if err != nil { primary.Close() return nil, err } // 初始使用主池 return &FailoverDB{ primary: primary, standby: standby, current: primary, }, nil } func (f *FailoverDB) Exec(query string, args ...interface{}) (sql.Result, error) { f.mu.RLock() db := f.current f.mu.RUnlock() res, err := db.Exec(query, args...) if err != nil && isConnectionError(err) { // 检测到连接错误,尝试切换到备池 f.mu.Lock() f.current = f.standby f.mu.Unlock() // 重试一次备池 return f.standby.Exec(query, args...) } return res, err } // 实现其他需要的方法如Query、QueryRow等
然后在主函数中初始化这个自定义DB,传入路由器:
func main() { db, err := NewFailoverDB(primaryDSN, standbyDSN) if err != nil { log.Fatal(err) } router := newRouter(db) http.ListenAndServe(":3333", router) }
优势:
- 对业务代码无侵入:处理器只需要调用封装后的方法,无需关心主备切换逻辑
- 可扩展性强:可以添加自动检测主库恢复、切回主池的逻辑
- 统一管理:所有切换逻辑集中在一处,便于维护
2. 使用数据库驱动自带的故障转移功能
部分PostgreSQL驱动支持配置多个连接地址,驱动内部自动处理故障转移和负载均衡。例如使用pgxpool:
func newConnection() *pgxpool.Pool { cfg, err := pgxpool.ParseConfig("postgres://user:pass@localhost,backup-host/bookstore") if err != nil { log.Fatal(err) } // 配置故障转移策略 cfg.ConnConfig.FallbackApplicationName = "bookstore-failover" pool, err := pgxpool.NewWithConfig(context.Background(), cfg) if err != nil { log.Fatal(err) } return pool }
优势:
- 无需自己实现切换逻辑:驱动层面已经处理了故障转移
- 成熟稳定:驱动的故障转移逻辑经过充分测试,可靠性更高
- 配置简单:只需要在DSN中添加多个地址即可
3. 使用外部代理实现数据库高可用
通过中间件实现数据库的自动故障转移,应用层只需要连接到代理地址,无需关心后端主备切换。
优势:
- 应用层完全无感知:代码不需要做任何修改,由代理负责主备切换和负载均衡
- 统一管理:所有数据库实例的高可用由代理统一维护
- 支持更多高级功能:如读写分离、连接池管理等
内容的提问来源于stack exchange,提问作者Jeremy Kenn
相关产品推荐
相关产品推荐

