时间:2021-07-01 10:21:17 帮助过:28人阅读
1、主从都使用读写账户,sql语句采用字符串拼接方式(废弃)
既不能实现主从读写分离,也不能防止sql注入攻击。
2、主从都使用读写账户,sql采用prepare+execute的方式(存在风险)
可以防止sql注入攻击,但不能实现主从读写分离,当并发量上来后主库压力会很大,存在风险。
3、主采用读写账户,从采用只读账户,sql采用prepare+execute的方式(会报错)
(1)首先抛出报错:
(2)报错原因分析:
图1-2 报错原因分析
错误原因:如果prepare的时候sql发给了从库1,但是execute的时候因为从库1延时较大,sql命令发给了从库2,就会报上面这个错误(除此之外,默认proxy会隔断时间切换备机,保证备机都能用到)。
4、主采用读写账户,从采用只读账户,建立mysql链接时使用参数interpolateParams=true
针对方案4,我们需要来看下底层源码:
这里我们需要关注两部分源码:database/sql和go-sql-driver/mysql,其中database/sql是golang针对数据库抽象出来的一个标准库,go-sql-driver/mysql是实现database/sql驱动接口的mysql驱动。
(1)我们一般写查询语句是这样的(举个栗子)
rows, err := db_slave.Query("SELECT * FROM tbl_user WHERE user_id = ?", 1)
if err != nil {
panic(err.Error()) //只是举例,真实使用中需要对错误进行处理和返回
}
我们知道prepared statement可以防止sql注入攻击,上图这样的写法看上去是使用的prepared statement方式,但到底是不是呢?我们继续往底层走。
(2)database/sql中查询接口是下面两个方法(即Query和QueryRow)
// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
注:Query执行查询并返回多个数据行,这个查询通常是一个select,方法中args参数用于填写查询语句中包含的占位符的实际参数。
// QueryRow executes a query that is expected to return at most one row.
// QueryRow always returns a non-nil value. Errors are deferred until
// Row‘s Scan method is called.
// If the query selects no rows, the *Row‘s Scan will return ErrNoRows.
// Otherwise, the *Row‘s Scan scans the first selected row and discards
// the rest.
func (db *DB) QueryRow(query string, args ...interface{}) *Row {
return db.QueryRowContext(context.Background(), query, args...)
}
注:QueryRow与Query方法不同点是,执行一条查询最多只会返回一个数据行。
(3)发现宝藏:Query底层执行查询的策略
从queryDC方法来看Query默认是不使用prepared statement方式的,只有在查询时发生driver.ErrSkip错误才会启用prepared statement继续查询。
注:从Query到queryDC方法经过的连接获取和错误处理等逻辑不影响我们分析问题,可以忽略。
// queryDC executes a query on the given connection.
// The connection gets released by the releaseConn function.
// The ctx context is from a query method and the txctx context is from an
// optional transaction context.
func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []interface{}) (*Rows, error) {
queryerCtx, ok := dc.ci.(driver.QueryerContext)
var queryer driver.Queryer
if !ok {
queryer, ok = dc.ci.(driver.Queryer)
}
if ok {
var nvdargs []driver.NamedValue
var rowsi driver.Rows
var err error
withLock(dc, func() {
nvdargs, err = driverArgsConnLocked(dc.ci, nil, args)
if err != nil {
return
}
//核心查询 不使用‘prepared Statement‘来执行
rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs)
})
if err != driver.ErrSkip {//发生错误driver.ErrSkip才使用‘prepared Statement‘方式去查询
if err != nil {//其他错误,关闭链接并报错返回
releaseConn(err)
return nil, err
}
// Note: ownership of dc passes to the *Rows, to be freed
// with releaseConn.
rows := &Rows{
dc: dc,
releaseConn: releaseConn,
rowsi: rowsi,
}
rows.initContextClose(ctx, txctx)
return rows, nil
}
}
//发生错误driver.ErrSkip时,才会使用‘Prepared Statement‘方式再次执行查询
var si driver.Stmt
var err error
withLock(dc, func() {
si, err = ctxDriverPrepare(ctx, dc.ci, query)//prepare
})
if err != nil {
releaseConn(err)
return nil,