go-zero 框架基于原生*sql.DB做了一整套封装,通过四层递进流程完成数据库连接的创建、缓存、增强包装与接口抽象,内置懒加载、单飞请求合并、熔断降级、监控采集能力。下面结合框架源码分步拆解整个连接构建流程。
1:创建sql.DB对象
core/stores/sqlx/sqlmanager.go
func newDBConnection(driverName, datasource string) (*sql.DB, error) {
conn, err := sql.Open(driverName, datasource)
if err != nil {
return nil, err
}
........
if err := conn.Ping(); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}
创建并初始化一个标准库 *sql.DB 连接池,然后用conn.Ping()测试数据连接有效性,因为sql.Open 只管创建内存对象,不建真实连接。
2:sql.DB对象哈希缓存
然后把 *sql.DB对象存进resources map[string]io.Closer,并暴露外部调用方法getSqlConn
func getCachedSqlConn(driverName, server string) (*sql.DB, error) {
val, err := connManager.GetResource(server, func() (io.Closer, error) {
conn, err := newDBConnection(driverName, server)
if err != nil {
return nil, err
}
if driverName == mysqlDriverName {
if cfg, e := mysql.ParseDSN(server); e != nil {
// if cannot parse, don't collect the metrics
logx.Error(e)
} else {
checksum := sha256.Sum256([]byte(server))
connCollector.registerClient(&statGetter{
host: cfg.Addr,
dbName: cfg.DBName,
hash: hex.EncodeToString(checksum[:]),
poolStats: func() sql.DBStats {
return conn.Stats()
},
})
}
}
return conn, nil
})
if err != nil {
return nil, err
}
return val.(*sql.DB), nil
}
func getSqlConn(driverName, server string) (*sql.DB, error) {
conn, err := getCachedSqlConn(driverName, server)
if err != nil {
return nil, err
}
return conn, nil
}
这里多说一下,因为go-zero数据库sql.DB对象是赖加载,即应用程序启动时,不一定要同时激活数据库连接。而是等到有请求业务涉及查询数据库时,才会创建slq连接对象,并缓存至哈希。这意味这,可能同时存在高并发场景,即同一时刻,大批量多个相同请求,都会去创建sql对象的创建、缓存,这无疑是灾难的。所以,go-zero的方案是:同一时刻相同请求合并,【具体请见:go-zero-同一时刻相同请求合并】。
缓存连接对象同时,connCollector.registerClient:注册监控采集器,数据库库地址、库名,定时采集池子运行指标,运维可通过监控查看连接池是否打满、是否大量等待连接。
注意,此时return conn是对象*sql.DB。
3:从哈希缓存中取出sql.DB,包装至结构体commonSqlConn。
\core\stores\sqlx\sqlconn.go
// NewSqlConn returns a SqlConn with given driver name and datasource.
func NewSqlConn(driverName, datasource string, opts ...SqlOption) SqlConn {
conn := &commonSqlConn{
connProv: func(context.Context) (*sql.DB, error) {
return getSqlConn(driverName, datasource)
},
onError: func(ctx context.Context, err error) {
logInstanceError(ctx, datasource, err)
},
beginTx: begin,
brk: breaker.NewBreaker(),
}
for _, opt := range opts {
opt(conn)
}
return conn
}
从getSqlConn取出sql.DB,赋值connProv,并包装至结构体commonSqlConn。
注意,此时这个conn是commonSqlConn结构体,里面的connProv就是sql.DB对象。
同时,该结构为还有一个熔断器,当sql执行之前,先通过熔断校验判断是否放行,sql执行完毕,熔断器也会更新统计状态等。
注册begin函数于beginTx字段,用途于事务场景。
4:实现 SqlConn 方法接口,以供业务层调用
最后返回的conn,即commonSqlConn结构体,return conn 后,就实现了SqlConn 接口方法,对外只暴露SqlConn统一方法(Query、Exec、Begin、Ping 等),隐藏commonSqlConn结构体字段,目的是隐藏内部缓存、熔断、获取连接的细节。
Session interface {
Exec(query string, args ...any) (sql.Result, error)
ExecCtx(ctx context.Context, query string, args ...any) (sql.Result, error)
Prepare(query string) (StmtSession, error)
..........
QueryRowsPartialCtx(ctx context.Context, v any, query string, args ...any) error
}
// SqlConn only stands for raw connections, so Transact method can be called.
SqlConn interface {
Session
...........
TransactCtx(ctx context.Context, fn func(context.Context, Session) error) error
}
以下代码段实现了SqlConn 接口所以方法,commonSqlConn结构体字段,只能在内部方法中访问,业务层完全调用不了,方便底层实现可以随意重构、修改字段、更换熔断算法等,而不影响上层业务。
func (db *commonSqlConn) Exec(q string, args ...any) (result sql.Result, err error) {
return db.ExecCtx(context.Background(), q, args...)
}
func (db *commonSqlConn) ExecCtx(ctx context.Context, q string, args ...any) (
result sql.Result, err error) {
ctx, span := startSpan(ctx, "Exec")
defer func() {
endSpan(span, err)
}()
err = db.brk.DoWithAcceptableCtx(ctx, func() error {
var conn *sql.DB
conn, err = db.connProv(ctx)
if err != nil {
db.onError(ctx, err)
return err
}
result, err = exec(ctx, conn, q, args...)
return err
}, db.acceptable)
if errors.Is(err, breaker.ErrServiceUnavailable) {
metricReqErr.Inc("Exec", "breaker")
}
return
}