![2de2333cf32ac00fcd369547c20a7db6.png](https://i-blog.csdnimg.cn/blog_migrate/bbf0c473ae9270106be8da00b70f3bed.png)
一、读写分离和防止sql注入的必要性(foreword)
1、 读写分离:
- 一句话定义:读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。
![7449927ab8c156b6e7ddd0ed282417f2.png](https://i-blog.csdnimg.cn/blog_migrate/19e3b01ccfed09da49bec806c3725084.png)
- 读写分离的好处:
(1) 增加冗余
(2) 增加了机器的处理能力
(3) 对于读操作为主的应用,使用读写分离是最好的场景,因为可以确保写的服务器压力更小,而读又可以接受点时间上的延迟。
2、 防止sql注入攻击:
一句话定义:SQL注入攻击(SQL Injection),简称注入攻击,是Web开发中最常见的一种安全漏洞。可以用它来从数据库获取敏感信息,或者利用数据库的特性执行添加用户,导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。
二、最佳解决方案(what)
主库用读写账户,从库用只读账户,建立mysql链接时使用参数interpolateParams=true
package
三、 几种方案对比(why)
1、主从都使用读写账户,sql语句采用字符串拼接方式(废弃)
既不能实现主从读写分离,也不能防止sql注入攻击。
2、主从都使用读写账户,sql采用prepare+execute的方式(存在风险)
可以防止sql注入攻击,但不能实现主从读写分离,当并发量上来后主库压力会很大,存在风险。
3、主采用读写账户,从采用只读账户,sql采用prepare+execute的方式(会报错)
(1)首先抛出报错:
![ff970b15f43828f058ce0d10d66284ab.png](https://i-blog.csdnimg.cn/blog_migrate/a1912c634044a2fcfee80c1330fdca4d.png)
(2)报错原因分析:
![23b6e6b155688ee731f9d9b06a25a458.png](https://i-blog.csdnimg.cn/blog_migrate/c569dd9645d47a2f2a2dbfc08e3b29b5.png)
错误原因:如果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.
注:Query执行查询并返回多个数据行,这个查询通常是一个select,方法中args参数用于填写查询语句中包含的占位符的实际参数。
// QueryRow executes a query that is expected to return at most one row.
注:QueryRow与Query方法不同点是,执行一条查询最多只会返回一个数据行。
(3)发现宝藏:Query底层执行查询的策略
从queryDC方法来看Query默认是不使用prepared statement方式的,只有在查询时发生driver.ErrSkip错误才会启用prepared statement继续查询。
注:从Query到queryDC方法经过的连接获取和错误处理等逻辑不影响我们分析问题,可以忽略。
// queryDC executes a query on the given connection.
(4)什么时候才会报driver.ErrSkip错误呢
答案就在go-sql-driver/mysql下的connection.go中真实Query方法中,请看大屏幕:
//mysql query底层实现
(5) 驱动程序是如何使用插值法防止sql注入攻击的呢?
我们到interpolateParams方法中一探究竟(我们只需要关注参数为string的情况):
func
源码中escapeStringBackslash方法的字面意思是对字符串参数转义,我们知道转义特殊字符就是防止sql注入攻击的有效方法之一,本着刨根问底的科学探索精神让我们再进到该方法中一探究竟~
//非常清晰,这里就是在做字符串中特殊字符的转义编码
这里就是在做字符串中特殊字符的转义编码,因此使用参数interpolateParams=true可以防止sql注入攻击,并且因为没有使用prepared statement方式,当采用主从读写分离方式时,也不会再报出方案3中的错误,既实现了主从读写分离也可以防止sql注入攻击,是最佳解决方案。
(6)interpolateParams=true使用注意事项
![66a8a2081663c483e4009ad8e016db19.png](https://i-blog.csdnimg.cn/blog_migrate/f49c7206515e6a8dad539f27a85971d2.jpeg)
可以看到官方文档最后给出一个补充说明,interpolateParams=true不可以与以下多字节编码共同使用(multibyte encodings BIG5, CP932, GB2312, GBK or SJIS),因为他们会引起sql注入脆弱性。
感谢阅读,如果你感觉本文能帮到你,欢迎点赞、评论和收藏本文,鄙人会再接再厉,为您带来更多系统架构和机器学习相关的知识分享~