欢送到我的博客中查看

线上有一块业务,须要做大量的数据库查问以及编码落盘的工作。数据库查问20分钟左右,大概有2kw条sql被执行。如果能够优化数据库查问的办法,能够节俭一笔很大的开销。

因为代码比拟长远,未能考据过后的数据查问选型为什么不实用orm,而是应用原生的形式本人构建。上面是外围的数据查问代码:

func QueryHelperOne(db *sql.DB, result interface{}, query string, args ...interface{}) (err error) {    // 数据库查问        var rows *sql.Rows        log.Debug(query, args)        rows, err = db.Query(query, args...)        if err != nil {                return err        }        defer rows.Close()        // 获取列名称,并转换首字母大写,用于和struct Field 匹配        var columns []string        columns, err = rows.Columns()        if err != nil {                return err        }        fields := make([]string, len(columns))        for i, columnName := range columns {                fields[i] = server.firstCharToUpper(columnName)        }    // 传参必须是数组 slice 指针        rv := reflect.ValueOf(result)        if rv.Kind() == reflect.Ptr {                rv = rv.Elem()        } else {                return errors.New("Parameter result must be a slice pointer")        }        if rv.Kind() == reflect.Slice {                elemType := rv.Type().Elem()                if elemType.Kind() == reflect.Struct {                        ev := reflect.New(elemType)            // 申请slice 数据,之后赋值给result                        nv := reflect.MakeSlice(rv.Type(), 0, 0)                        ignoreData := make([][]byte, len(columns))                        for rows.Next() { // for each rows                // scanArgs 是扫描每行数据的参数                // scanArgs 中存储的是 struct 中field 的指针                                scanArgs := make([]interface{}, len(fields))                                for i, fieldName := range fields {                                        fv := ev.Elem().FieldByName(fieldName)                                        if fv.Kind() != reflect.Invalid {                                                scanArgs[i] = fv.Addr().Interface()                                        } else {                                                ignoreData[i] = []byte{}                                                scanArgs[i] = &ignoreData[i]                                        }                                }                                err = rows.Scan(scanArgs...)                                if err != nil {                                        return err                                }                                nv = reflect.Append(nv, ev.Elem())                        }                        rv.Set(nv)                }        } else {                return errors.New("Parameter result must be a slice pointer")        }        return}

办法通过如下形式调用:

type TblUser struct {    Id          int64    Name        string    Addr        string    UpdateTime  string}result := []TblUser{}QueryHelperOne(db, &result, query, 10)

间接看下面的代码,发现没有什么大的问题,然而从细节上一直调优,能够让性能压迫到极致。

网络优化

golang 提供的db.Query(sql, args...) 办法,外部的实现,也是基于prepare 办法实现的。
prepare 有三个益处:

- 能够让 mysql 省去每次语法分析的过程- 能够避免出现sql 注入- 能够重复使用prepare 的后果,只发送参数即可做查问

然而,也有不好的中央。一次 db.Query 会有三次网络申请。

  • prepare
  • execute
  • closing

而如果有屡次雷同SQL 查问的话,这种形式是十分占优的。因而,能够应用prepare 替换 db.Query 缩小一次网络耗费。

var stmts = sync.Map{}func QueryHelperOne(db *sql.DB, result interface{}, query string, args ...interface{}) (err error) {    // 应用sync.Map 缓存 query 对应的stmt    // 缩小不必要的prepare 申请    var stmt *sql.Stmt    if v, ok := stmts.Load(query); ok {        stmt = v.(*sql.Stmt)    } else {        if stmt, err = db.Prepare(query); err != nil {            return err        } else {            stmts.Store(query, stmt)        }    }    var rows *sql.Rows    log.Debug(query, args)    rows, err = stmt.Query(args...)    if err != nil {        _ = stmt.Close()        stmts.Delete(query)        return err    }    defer rows.Close()    // 前面代码省略 ...}

通过此番批改,作业的性能晋升了17%,成果还是非常明显的。

GC 优化

优化1

在服务中,会预申请slice空间,因而无需每次构建的时候从新申请slice 内存。

// old code// nv := reflect.MakeSlice(rv.Type(), 0, 0)// new codenv := rv.Slice(0, 0)

优化2

从代码56 行能够看到,每次会append 数据到数组中。因为 构造体切片在append 时,是做内存拷贝;scanArgs 的数据因为每次scan 都会笼罩,因而能够复用,不须要每次rows 的时候映射。

ev := reflect.New(elemType)// 申请slice 数据,之后赋值给resultnv := reflect.MakeSlice(rv.Type(), 0, 0)ignoreData := make([][]byte, len(columns))// scanArgs 是扫描每行数据的参数// scanArgs 中存储的是 struct 中field 的指针scanArgs := make([]interface{}, len(fields))for i, fieldName := range fields {        fv := ev.Elem().FieldByName(fieldName)        if fv.Kind() != reflect.Invalid {                scanArgs[i] = fv.Addr().Interface()        } else {                ignoreData[i] = []byte{}                scanArgs[i] = &ignoreData[i]        }}for rows.Next() { // for each rows    err = rows.Scan(scanArgs...)        if err != nil {                return err        }        nv = reflect.Append(nv, ev.Elem())}rv.Set(nv)

缩小了每行扫描的时候,新申请scanArgs

优化 3

对于不在field中的数据,须要应用一个空的值代替,下面代码应用的是一个[]byte 的切片,其实只须要一个[]byte 即可。代码如下:

ignoreData := []byte{}// scanArgs 是扫描每行数据的参数// scanArgs 中存储的是 struct 中field 的指针scanArgs := make([]interface{}, len(fields))for i, fieldName := range fields {        fv := ev.Elem().FieldByName(fieldName)        if fv.Kind() != reflect.Invalid {                scanArgs[i] = fv.Addr().Interface()        } else {                scanArgs[i] = &ignoreData        }}

优化 4

因为雷同的sql会查问次数在千万级;因而能够把每次扫描行所须要的行元素ev,以及对应的扫描参数列表 scanArgs 都缓存起来,再应用时从内存中加载即可。

// 定义数据池,用于存储每个sql 对应的扫描行item 以及扫描参数// 全局代码var datapools = sync.Map{}type ReflectItem struct {    Item     reflect.Value    scanArgs []interface{}}///////// 办法调用外部// 从数据池中加载query 对应的 ReflectItemif v, ok := datapools.Load(query); ok {    pool = v.(*sync.Pool)} else {    // 构建reflectItem        var columns []string        columns, err = rows.Columns()        if err != nil {                return err        }    pool = &sync.Pool{        New: func() interface{} {            fields := make([]string, len(columns))            for i, columnName := range columns {                fields[i] = server.firstCharToUpper(columnName)            }            ev := reflect.New(elemType) // New slice struct element            // nv := reflect.MakeSlice(rv.Type(), 0, 0) // New slice for fill            ignored := []byte{}            scanArgs := make([]interface{}, len(fields))            for i, fieldName := range fields {                fv := ev.Elem().FieldByName(fieldName)                if fv.Kind() != reflect.Invalid {                    scanArgs[i] = fv.Addr().Interface()                } else {                    scanArgs[i] = &ignored                }            }            return ReflectItem{                Item:     ev,                scanArgs: scanArgs,            }        },    }    datapools.Store(query, pool)}ri = pool.Get().(ReflectItem)// 复用 ev 和 scanArgsev = ri.ItemscanArgs = ri.scanArgs// 开始扫描nv := rv.Slice(0, 0)for rows.Next() { // for each rows    err = rows.Scan(scanArgs...)    if err != nil {        return err    }    nv = reflect.Append(nv, ev.Elem())}rv.Set(nv) // return rows data back to callerpool.Put(ri)// 完结扫描

通过几次优化,24分钟执行完的作业,胜利缩小到了18分钟。

总结

  • golang prepare 的实现,须要进一步理解,在应用prepare的状况下,连贯是如何复用的,比拟困惑。
  • 对于雷同query 的状况,然而扫描struct 类型不同的状况,会有问题。扫描参数的数据池,应该应用构造体类型做key。