go database sql接口分析及sql埋点实现

大家好,我是蓝胖子,关于sql监控的需求有很多,比如我们常常需要在sql执行前后加上埋点来对sql执行时长,执行语句进行记录,今天我们就来看看在golang中如何实现sql的埋点记录。

首先,我们来看下,在golang中如何实现数据库查询。

golang 实现数据库查询

golang的database/sql包下封装了对数据库查询的接口方法,真正实现数据库连接以及查询的逻辑是由第三方库实现的,拿mysql举例,这个库是github.com/go-sql-driver/mysql。

通常我们执行一个sql语句会先创建一个sql.DB对象,代码如下,

go 复制代码
db, err = sql.Open(driverName, dataSourceName)

sql.Open方法要求传入驱动名称和数据库连接字符串,数据库驱动是由第三方库注册到全局变量里的,如下:

go 复制代码
// /usr/local/go/src/database/sql/sql.go:35
var (  
   driversMu sync.RWMutex  
   drivers   = make(map[string]driver.Driver)  
)

/// goproject/pkg/mod/github.com/go-sql-driver/mysql@v1.7.1/driver.go:83
func init() {  
   sql.Register("mysql", &MySQLDriver{})  
}

在执行sql时,一般是通过调用sql.DB对象的Query或者Exec方法,

go 复制代码
func (db *DB) Query(query string, args ...any) (*Rows, error) 
func (db *DB) Exec(query string, args ...any) (Result, error) 

下面我们就来着重的分析下这两个方法内部是如何执行sql的。

需要注意的是,在分析过程中一定要分清楚哪些是go官方包database/sql 下的逻辑,哪些是第三方库的逻辑,这样可以很好的理解go database/sql包是如何规范sql查询的。

接口分析

db.Query 方法最终会走到db.queryDc 方法,db.Exec最终会走到db.execDc方法,两个方法执行逻辑都是比较类似的,如下所示:

它们首先都会判断能不能直接执行sql语句,如果不能的话就要使用占位符的方式,prepare和statement的方式来执行sql,返回dirver.ErrSkip错误则代表前者的执行逻辑走不通,需要跳过,接着执行下面的逻辑。

无论是直接执行sql还是使用prepare和statement的方式执行sql,其内部最终都会调用到第三方库封装好的操作数据库的方法。

拿query逻辑,github.com/go-sql-driver/mysql 库举例,database/sql包下用到的连接接口类型实际上是github.com/go-sql-driver/mysql 第三方库返回的连接结构体,所以如果第三方库实现的连接实现类型实现了相应的driver.Queryer或者driver.QueryerContext接口,那么查询时则会走到直接执行sql的方法ctxDriverQuery里。

所以对于第三方库的连接类型实现而言,driver.Queryer接口并不是必需的,database/sql对于连接类型的定义接口只要包含如下几个方法就可以了:

go 复制代码
type Conn interface {  
	Prepare(query string) (Stmt, error) 
	Close() error  
	Begin() (Tx, error)  
}

不过github.com/go-sql-driver/mysql 依然为连接类型实现了driver.Queryer接口方法,在这个方法里,由它自己去判断是否能够采取直接执行sql的方式。

直接执行sql相比prepare statement 方式少一次网络传输,效率更高,但会带来sql注入问题。

所以github.com/go-sql-driver/mysql 实现driver.Queryer接口时,判断了 如果写的sql有传参数,比如像这样db.Query("select * from t_user where id = ?;",1),需要将连接配置参数InterpolateParams设置为true才能采取直接执行sql的方式,否则会返回dirver.ErrSkip ,让databse/sql包去执行prepare statement的逻辑。

连接配置参数InterpolateParams设置为true ,github.com/go-sql-driver/mysql 会对sql语句执行转义,避免sql注入。

看到这里,应该明白了golang中执行sql的逻辑,我们的最终目的是对sql执行前后能够加上一些自己的埋点日志。所以接下来,还要看看这部分应该如何来做。

自定义驱动实现sql埋点统计

由于database/sql包下仅仅是定义了一个连接类型的接口,连接类型的真正实现是由第三方库实现的,所以我们可以采取装饰器模式,对github.com/go-sql-driver/mysql 库产生的连接类型进行重新包装,覆盖原有的查询方法,就可以在查询前后加上自己的埋点逻辑了。

连接的创建是由驱动产生的,database/sql包下同样也只定义了一个驱动接口,实现是第三方库实现的。接口如下:

go 复制代码
type Driver interface {  
    Open(name string) (Conn, error)  
}

并且database/sql包下还有一个全局变量drivers和一个注册方法,第三方库可以通过这个注册方法把自己实现的驱动注册进去,后续database/sql包下创建连接时,就是通过驱动名创建一个sql.DB 对象,sql.DB对象内部会用对应的驱动实现创建连接。

go 复制代码
// /usr/local/go/src/database/sql/sql.go:35
var (  
   driversMu sync.RWMutex  
   drivers   = make(map[string]driver.Driver)  
)
/// goproject/pkg/mod/github.com/go-sql-driver/mysql@v1.7.1/driver.go:83
func init() {  
   sql.Register("mysql", &MySQLDriver{})  
}
// 用户代码
db, err = sql.Open(driverName, dataSourceName)

所以我们要实现自定义的连接类型来包装github.com/go-sql-driver/mysql 库下的连接类型,首先是包装它的驱动类型。如下,我们可以对原有连接和驱动加上钩子函数的属性。

go 复制代码
type Hooks interface {  
   Before(ctx context.Context, query string, args ...interface{}) (context.Context, error)  
   After(ctx context.Context, query string, args ...interface{}) (context.Context, error)  
}

type Driver struct {  
   driver.Driver  
   hooks Hooks  
}  
  
func (drv *Driver) Open(name string) (driver.Conn, error) {  
   conn, err := drv.Driver.Open(name)  
   if err != nil {  
      return conn, err  
   }  
   wrapped := &Conn{conn, drv.hooks}  
   return wrapped, nil  
}  
  
Conn struct {  
   driver.Conn  
   hooks Hooks  
}

对于新的链接类型只要对它的查询方法进行覆盖就能埋点了,拿query接口举例,在原有的queryContext方法前后加上自己的逻辑。

go 复制代码
func (conn *Conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {  
   var err error  
   list := namedToInterface(args)  
   // Query `Before` Hooks  
   if ctx, err = conn.hooks.Before(ctx, query, list...); err != nil {  
      return nil, err  
   }  
   results, err := conn.queryContext(ctx, query, args)  
   if err != nil {  
      return results, handlerErr(ctx, conn.hooks, err, query, list...)  
   }  
   if _, err := conn.hooks.After(ctx, query, list...); err != nil {  
      return nil, err  
   }  
   return results, err  
}

注意,因为用连接查询的地方涉及到好几个接口方法,我们都需要覆盖到这些方法,才能让埋点不会被漏掉。

这些地方分别是queryDC中执行执行sql时的QueryContext 接口,execDc中执行sql时的ExecContext接口,连接的PrepareContext接口,以及driver.Stmt的ExecContext和QueryContext接口。

具体的实现钩子函数的代码已经上传到github

shell 复制代码
https://github.com/HobbyBear/easymonitor/blob/main/infra/sqlhooks.go
相关推荐
r i c k1 小时前
数据库系统学习笔记
数据库·笔记·学习
野犬寒鸦1 小时前
从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解
java·jvm·数据库·后端·学习
IvorySQL2 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·2 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德2 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫3 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i3 小时前
完全卸载MariaDB
数据库·mariadb
纤纡.3 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn3 小时前
【Redis】渐进式遍历
数据库·redis·缓存
橙露3 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot