关于golang:记一次-Golang-数据库查询组件的优化

42次阅读

共计 4678 个字符,预计需要花费 12 分钟才能阅读完成。

欢送到我的博客中查看

线上有一块业务,须要做大量的数据库查问以及编码落盘的工作。数据库查问 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 code
nv := rv.Slice(0, 0)

优化 2

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

ev := reflect.New(elemType)
// 申请 slice 数据,之后赋值给 result
nv := 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 对应的 ReflectItem
if 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 和 scanArgs
ev = ri.Item
scanArgs = 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 caller
pool.Put(ri)
// 完结扫描 

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

总结

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

正文完
 0