`
cs_css
  • 浏览: 84146 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论
阅读更多

一.背景:

用Mybatis+mysql的架构做开发,大家都知道,Mybatis内置参数,形如#{xxx}的,均采用了sql预编译的形式,举例如下:

<select id=”aaa” parameterType=”int” returnType=”Blog”>
                   select * from blog where id = #{id}
    </select>

 

         查看日志后,会发现这个sql执行时被记录如下:

         

select * from blog where id =?

 

        之前上网查过一些资料,大致知道mybatis底层使用PreparedStatement,过程是先将带有占位符(即”?”)的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,然后直接执行带有真实参数的sql。网上关于这个问题的资料较少,但基本结论是,使用预编译,可以提高sql的执行效率,并且有效地防止了sql注入。我一直对这个结论深信不疑,直到看了一篇名叫Java中连结MySQL启用预编译的先决条件是useServerPstmts=true.”的文章,说mysql是默认不开启预编译的,大多数持久层框架(例如mybatis)和jdbc代码都没有做到真实开启预编译,文章链接如下:http://blog.csdn.net/axman/article/details/6913527  ,另外还有一些文章说mysql是不支持预编译的,总之,众说纷纭,因为项目中使用的就是mysql,所以我决定花一些时间来探究一下这个问题。

二.问题:

       我的疑问有两点:1.mysql是否默认开启了预编译功能?若没有,将如何开启? 2.预编译是否能有效地节省sql执行的成本

三.探究一:mysql是否默认开启预编译?

       首先针对第一个问题。懒得开linux虚拟机了,我电脑上已经安装了mysql,版本是5.0.18,打开配置文件my.ini,在“port=3306“这一行下面加了配置:log=d:/logs/mysql50_log.txt,这样就开启了mysql日志功能,该日志主要记录mysql执行sql的过程。重启mysql,并建立一个库prepare_stmt_test,在该库下建一个叫users的表,有id(主键)和name两个字段。

         接着,我建立了一个简单的java工程,引入jdbc驱动包mysql-connector-java-5.0.3-bin.jar。然后写了如下的代码:

        

 public static void main(String []a) throws Exception{
        String sql = "select * from users where name = ?";
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = null;
        try{
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root");
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setString(1, "aaa");
            ResultSet rs = stmt.executeQuery();
            rs.close();
            stmt.close();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            conn.close();
        }
    }

 

    执行这些代码后,打开刚才配置的mysql日志文件mysql50_log.txt,日志记录如下:

    1 Query       SET NAMES utf8

                         1 Query       SET character_set_results = NULL

                         1 Query       SHOW VARIABLES

                         1 Query       SHOW COLLATION

                         1 Query       SET autocommit=1

                         1 Prepare     [1]

                         1 Execute     [1] select * from users where name = 'aaa'

                         1 Quit      

         日志格式有点奇怪,明明打出了prepare关键字,但没有我设定的预编译的语句“select * from users where name = ?”,更令人疑惑的是,刚才说的那篇名叫“Java中连结MySQL启用预编译的先决条件是useServerPstmts=true.”的文章里提到的,若jdbc连接没有加useServerPrepStmts =truemysql日志里连prepare关键字都不会记录。而我的测试结果是,不加useServerPrepStmts =trueprepare关键字是有的,但没有预编译的sql模板“select * from users where name = ?

         可能是我的mysql版本比较老吧,于是我停掉mysql5.0服务,安装了mysql5.5,依照刚才那样建库建表,并启用了一个新的mysql日志文件mysql55_log.txt。一切OK后,我又一次执行了刚才的测试程序,然后打开日志文件mysql55_log.txt,发现了这样的记录:

            1 Query      SET NAMES utf8

                       1 Query    SET character_set_results = NULL

                       1 Query    SHOW VARIABLES

                       1 Query    SHOW WARNINGS

                       1 Query    SHOW COLLATION

                       1 Query    SET autocommit=1

                       1 Prepare          select * from users where name = ?

                       1 Execute          select * from users where name = 'aaa'

                       1 Close stmt   

                       1 Quit      

         终于看到sql模板“select * from users where name = ?”了,但仍然有很多疑惑,首先,刚才的mysql5.0到底开启预编译了吗?其次,我并没有加useServerPrepStmts =true配置,但mysql5.5的确是做了预编译的操作的,这与“Java中连结MySQL启用预编译的先决条件是useServerPrepStmts =true.<!--[if !supportNestedAnchors]--><!--[endif]-->”这篇文章的测试结果大相径庭。

         带着这些问题,又仔细阅读了一下CSDN上这篇文章,作者的结论是:jdbc连接mysql时配置useServerPrepStmts参数为true后才能开启mysql预编译功能。看来这个useServerPrepStmts参数是很重要的,于是我将刚才代码里的jdbc连接修改如下:

DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts =false")

 

执行代码后,再次查看mysql日志:

1 Query    SET NAMES utf8

                       1 Query    SET character_set_results = NULL

                       1 Query    SHOW VARIABLES

                       1 Query    SHOW WARNINGS

                       1 Query    SHOW COLLATION

                       1 Query    SET autocommit=1

                       1 Query    select * from users where name = 'aaa'

130410 15:06:48          1 Quit      

果然,日志了没有了prepare这一行,说明mysql没有进行预编译。这意味着useServerPrepStmts这个参数是起效的,且默认值为true。那么,为什么在刚才那篇文章里,作者得出的结论是useServerPrepStmts默认为false呢?

继续思考了一阵,我突然意识到,useServerPrepStmts这个参数是jdbc的连接参数,这说明此问题与jdbc驱动程序可能有关系。打开mysql官网,发现在线的官方文档很强大,支持全文检索,于是我将“useServerPrepStmts”做为关键字,搜索出了一些信息,原文如下:

Important change: Due to a number of issues with the use of server-side prepared statements, Connector/J 5.0.5 has disabled their use by default. The disabling of server-side prepared statements does not affect the operation of the connector in any way.

To enable server-side prepared statements, add the following configuration property to your connector string:

useServerPrepStmts=true

The default value of this property is false (that is, Connector/J does not use server-side prepared statements)

       这段文字说,Connector/J5.0.5以后的版本,默认useServerPrepStmts参数为falseConnector/J就是我们熟知的jdbc驱动程序。看来,如果我们的驱动程序为5.0.5或之后的版本,想启用mysql预编译,就必须设置useServerPrepStmts=true。我的jdbc驱动用的是5.0.3,这个版本的useServerPrepStmts参数默认值是true。于是我将java工程中的jdbc驱动程序替换为5.0.8的版本,去掉代码里jdbc连接中的useServerPrepStmts参数,再执行,发现mysql5.5的日志打印如下:

           2 Query         SHOW SESSION VARIABLES

                       2 Query    SHOW WARNINGS

                       2 Query    SHOW COLLATION

                       2 Query    SET NAMES utf8

                       2 Query    SET character_set_results = NULL

                       2 Query    SET autocommit=1

                       2 Query    select * from users where name = 'aaa'

                       2 Quit      

         那么,mysql5.0呢?我停掉mysql5.5服务,开启mysql5.0,再执行java代码,查看mysql5.0的日志:

         1 Query       SHOW SESSION VARIABLES

                         1 Query       SHOW COLLATION

                         1 Query       SET NAMES utf8

                         1 Query       SET character_set_results = NULL

                         1 Query       SET autocommit=1

                         1 Query       select * from users where name = 'aaa'

                         1 Quit 

         果然,在mysql5.0日志里,prepare关键字没有了。mysql5.0的日志格式和mysql5.5的不太一样,5.0日志只打印一个“prepare”关键字,而不打印预编译sql模板。

   第一个问题解决了,结论就是:mysql是否默认开启预编译,与MySQL server的版本无关,而与 MySQL Connector/J(驱动程序)的版本有关,Connector/J 5.0.5及以后的版本默认不支持预编译,Connector/J 5.0.5之前的版本默认支持预编译。

四.探究二:预编译是否能有效地节省sql执行的成本?

       首先,我们要明白mysql执行一个sql语句的过程。查了一些资料后,我得知,mysql执行脚本的大致过程如下:prepare(准备)-> optimize(优化)-> exec(物理执行),其中,prepare也就是我们所说的编译。开篇时已经说过,对于同一个sql模板,如果能将prepare的结果缓存,以后如果再执行相同模板而参数不同的sql,就可以节省掉prepare(准备)的环节,从而节省sql执行的成本。明白这一点后,我写了如下测试程序:

      

 public static void main(String []a) throws Exception{
       String sql = "select * from users where name = ?";
       Class.forName("com.mysql.jdbc.Driver");
       Connection conn = null;
       try{
           conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true");
           PreparedStatement stmt = conn.prepareStatement(sql);
           stmt.setString(1, "aaa");
           ResultSet rs1 = stmt.executeQuery();//第一次执行
rs1.close();
           stmt.setString(1, "ddd");
           ResultSet rs2 = stmt.executeQuery();//第二次执行
           rs2.close();
           stmt.close();
       }catch(Exception e){
           e.printStackTrace();
       }finally{
           conn.close();
       }
    }

 

    执行该程序后,查看mysql日志:

1 Query    SHOW SESSION VARIABLES

                       1 Query    SHOW WARNINGS

                       1 Query    SHOW COLLATION

                       1 Query    SET NAMES utf8

                       1 Query    SET character_set_results = NULL

                       1 Query    SET autocommit=1

                       1 Prepare          select * from users where name = ?

                       1 Execute          select * from users where name = 'aaa'

                       1 Execute          select * from users where name = 'ddd'

                       1 Close stmt   

                       1 Quit      

按照日志看来,PreparedStatement重新设置sql参数后,并没有重新prepare,看来预编译起到了效果。但刚才我使用的是同一个stmt,如果将stmt关闭呢?

public static void main(String []a) throws Exception{
       String sql = "select * from users where name = ?";
       Class.forName("com.mysql.jdbc.Driver");
       Connection conn = null;
       try{
           conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true");
           PreparedStatement stmt = conn.prepareStatement(sql);
           stmt.setString(1, "aaa");
           ResultSet rs1 = stmt.executeQuery();//第一次执行
           rs1.close();
           stmt.close();
           stmt = conn.prepareStatement(sql);//重新获取一个statement
           stmt.setString(1, "ddd");
           ResultSet rs2 = stmt.executeQuery();//第二次执行
           rs2.close();
           stmt.close();
       }catch(Exception e){
           e.printStackTrace();
       }finally{
           conn.close();
       }
    }

 

mysql日志打印如下:

1 Query    SHOW SESSION VARIABLES

                       1 Query    SHOW WARNINGS

                       1 Query    SHOW COLLATION

                       1 Query    SET NAMES utf8

                       1 Query    SET character_set_results = NULL

                       1 Query    SET autocommit=1

                       1 Prepare          select * from users where name = ?

                       1 Execute          select * from users where name = 'aaa'

                       1 Close stmt   

                       1 Prepare          select * from users where name = ?

                       1 Execute          select * from users where name = 'ddd'

                       1 Close stmt   

                       1 Quit

很明显,关闭stmt后再执行第二个sqlmysql就重新进行了一次预编译,这样是无法提高sql执行效率的。而在实际的应用场景中,我们不可能保持同一个statement。那么,mysql如何缓存预编译结果呢?

搜索一些资料后得知,jdbc连接参数中有另外一个重要的参数:cachePrepStmts,设置为true后可以缓存预编译结果。于是我将测试代码中jdbc连接串改为了这样:

          

 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true&cachePrepStmts=true");

 

再执行代码后,发现mysql日志记录又变成了这样:

1 Prepare          select * from users where name = ?

                           1 Execute          select * from users where name = 'aaa'

                       1 Execute          select * from users where name = 'ddd'

OK,现在我们开启了预编译,并开启了缓存预编译的功能,那么开始性能测试。我向刚才的单表中插入了10000条数据,并做10000次同样sql模板,不同参数的select。记录结果如下:

当不开启预编译功能时,做5次测试,10000select总时间为(单位毫秒):

49172,49172,49000,49047,48922

开启预编译,但不开启预编译缓存时,测试数值如下:

50797,50860,50672,50750,50703

开启预编译,开启预编译缓存,测试数值如下:

49547,49250,49593,49093,49078.

从测试结果看来,若开启预编译,但不开启预编译缓存,查询效率会有明显下降;但开启预编译且开启预编译缓存时,查询效率比不开启缓存有提高,但和不开启预编译基本持平。

结论出来了:对于Connector/J5.0.5以上的版本,若使用useServerPrepStmts=true开启预编译,则一定要同时使用cachePrepStmts=true 开启预编译缓存,否则性能会下降,若二者都开启,性能并没有显著的提高,这个可能是我测试程序的原因,有待进一步研究。

五.总结:

       经过这一系列的探究,能够得出一些结论:

首先批一下《Java中连结MySQL启用预编译的先决条件是useServerPstmts=true.》这篇文章吧,文章写得很不科学,作者并没有关注mysqlConnector/J的版本之间的差异,对于mysql,他说mysql一定支持预编译,事实上,经过我查询官方文档后,得知MySQL Server 4.1之前的版本是不支持预编译的;对于Connector/J,他也没有关注5.0.5这个版本节点。所以,虽然被浏览转载了很多次,但这篇文章的结论仍然是错误的,应该也误导了不少人;对于开启预编译和预编译缓存后对性能的影响,这篇文章也没有涉及,事实上经过我测试,若jdbc驱动是5.0.5及之后的版本,同时开启预编译和预编译缓存,sql的执行性能并没有显著提高,若jdbc驱动是5.0.5之前的版本,默认开启了预编译,则一定要加cachePrepStmts=true,否则mysql的执行效率会比较低。总之,预编译和预编译缓存一定要同时开启或同时关闭,不同Connector/J的版本,useServerPrepStmts的默认值会有所不同。

再谈谈SQL预编译这个东西,其实“预编译”这个叫法不是很准确,官方文档里把它叫做“预准备”。经过我测试,对于mysql,开启了预编译缓存后,不同connection之间,预编译的结果是独立的,是无法共享的,一个connection无法得到另外一个connection的预编译缓存结果,对于这一点,我想mysql的开发人员应该会在以后逐步改进吧。再一点,关于预编译缓存的内容,我查了相关的资料后得知,mysql执行一个预编译操作后,会将sql模板(即带占位符“?”的sql),以及参数列表(模板中用各个占位符表示)缓存,下一次有同样的sql模板发来的时候,直接将参数传给这个模板,拼好后execute。虽然mysql的预编译功能对性能影响不大,但在jdbc中使用PreparedStatement是必要的,可以有效地防止sql注入,这一点大家都明白。

 

分享到:
评论
4 楼 ismallboy 2017-11-09  
您好,请问一下,这个服务端的预编译是针对session的,如果是断开连接,再执行的话,又要重新预编译了。但是我们普遍的场景都是每次执行完都是断开连接的,那岂不是这个预编译的意义不大?还是说这个预编译针对批量操作的时候(不是频繁操作)才有意义?(即使有数据库连接池,也是有很多connection的)
3 楼 sharefling 2015-12-26  
good,良心
2 楼 huangningren 2015-01-13  
真棒!market下!
1 楼 red_devils 2013-09-23  
分析得很精彩,很细致,学习了!

相关推荐

    mysql MySQL数据库开发优化与管理维护

    - **PreparedStatement + Bind-variable**:使用预编译语句来防止SQL注入。 - **使用应用程序提供的转换函数**:通过内置函数过滤输入数据。 - **自己定义函数进行校验**:编写自定义函数确保数据的安全性。 ###...

    基于Windows的MySQL体系结构分析与性能优化技术的研究.doc

    3. 探究MySQL的性能优化技术,包括如何有效地利用索引提高查询速度,如何调整系统参数和硬件配置以提升整体性能,以及如何利用高级特性来优化数据操作。 在研究方法上,将采用理论研究与实践操作相结合的方式,通过...

    php_mysql留言板

    PHP通过预编译的SQL语句与MySQL进行通信。例如,使用MySQLi扩展的代码示例: ```php // 连接数据库 $conn = new mysqli('localhost', 'username', 'password', 'database'); // 检查连接 if ($conn-&gt;connect_error...

    反编译后的完整的倾城源码

    在IT行业中,逆向工程是一种技术手段,通常用于理解已编译的二进制程序的工作原理,而源码分析则是在获取到源代码后,对代码逻辑、结构和功能进行深入探究的过程。 倾城源码,可能是一个游戏、应用程序或者服务的...

    探究php底层运行机制 - shupan001的专栏 - 博客频道 - CSDN.NET1

    扩展层则提供了一系列特定功能的函数和类库,例如,MySQL扩展用于与MySQL数据库交互。 请求执行完毕后,PHP会进入清理阶段,调用RSHUTDOWN方法,清理符号表,释放内存,以及执行其他必要的清理工作。RSHUTDOWN方法...

    鲤鱼论坛 v1.0(附源码)

    9. **安全防护**:防止SQL注入、XSS攻击和CSRF(跨站请求伪造)等安全威胁,需要进行输入验证和使用预编译语句。 10. **API接口设计**:如果论坛支持移动应用或第三方接入,那么API设计和版本控制就很重要,遵循...

    Facemash-源码.rar

    这可能通过使用预编译的SQL语句、过滤输入数据或使用安全的编程实践来实现。 6. 性能优化:随着用户数量的增长,性能优化变得至关重要。源码可能包含了缓存机制、负载均衡策略,以及针对数据库查询的优化,以确保...

    php毕业设计之企业宣传网站源码(完整源码).zip

    8. **安全措施**:了解如何防止SQL注入、XSS攻击等网络安全问题,例如使用预编译语句、过滤用户输入、正确编码输出等。 9. **响应式设计**:企业宣传网站通常需要适应不同设备的屏幕尺寸,因此源码可能使用了...

    PHP扬州交友完整源码

    理解如何进行输入验证、使用预编译语句、转义特殊字符等,确保网站安全性。 7. **文件上传与管理**:交友网站往往需要用户上传头像或照片,因此源码中应包含文件上传功能,学习如何限制文件类型、大小,以及如何...

    很漂亮大方的留言板

    同时,系统应具备防止SQL注入、XSS攻击的能力,通过验证输入数据和使用预编译语句等方式提高安全性。 4. **API接口**:如果留言板需要与其他系统集成,比如社交媒体分享功能,那么可能需要设计API接口。RESTful API...

    数据库 万能查询程序源代码

    5. **性能优化**:程序可能包含优化查询性能的机制,例如预编译语句、批量执行等,以提高数据检索速度。 6. **安全特性**:确保用户只能访问他们被授权的数据,避免数据泄露或误操作。 源代码的附带数据库文件可能...

    Borland C++ Builder 6.0 Enterprise 7CD光盘镜像:eLearning.iso

    Borland C++ Builder 6.0 Enterprise的核心在于其直观的Visual Component Library (VCL)框架,这是一个面向对象的用户界面库,包含大量预构建的组件,可用于快速构建Windows应用程序。VCL将C++的性能与图形用户界面...

    计算机软件-编程源码-YourBase2003资料管理.zip

    【标题】"计算机软件-编程源码-YourBase2003资料管理.zip" 提供的...通过对这些知识点的深入探究,无论是为了学习编程技巧、研究历史软件设计,还是复用部分代码,YourBase2003资料管理的源码都提供了丰富的学习材料。

    apache-hive-1.1.1-src.tar

    1. **元数据存储**:Hive 使用 MySQL 或者其他兼容的 RDBMS 存储元数据,如表名、列名、分区信息等。这些信息是执行查询时的关键。 2. **HiveQL**:Hive 的查询语言,类似于 SQL,允许用户定义数据结构、加载数据、...

    毕业网页

    C++应用需要考虑SQL注入、XSS攻击、CSRF等常见Web安全风险,并采取相应的防护措施,如使用预编译语句防止SQL注入,过滤用户输入避免XSS等。 7. **部署与运维**:C++ Web应用的部署通常涉及到Nginx或Apache等反向...

    java从入门到精通(韩顺平视频)笔记整理

    学习Java时,应遵循一些有效的方法,例如高效愉快地学习,先建立整体框架再深入细节,根据实际需求选择学习的内容,先理解如何操作再探究其原因,实践是掌握编程技能的关键。此外,借鉴他人的经验也很重要,但同时要...

    JAVA自学之路 七路线图明细

    学习Annotation的使用,Ant的编译与部署,EJB Session Bean,依赖注入,以及Persistence API。 在自学过程中,推荐先通过视频教程入门,再选择适合自己的书籍进行深入学习。可以通过图书销售排行榜来筛选优质书籍,...

    商业源码-编程源码-Gart v1.3 gbk build 20101226.zip

    在这款源码中,我们可以深入探究到当时商业软件开发的常用技术和策略,以及GBK编码在多语言支持中的应用。 1. **Gart项目概述**: Gart项目可能是一个具有广泛功能的商业级应用程序,其版本号v1.3表明它经过了多次...

    iris1.8源码

    《Iris 1.8源码解析:网络游戏开发的深度探究》 Iris 1.8源码是一款基于C++编程语言开发的网络游戏框架,它为开发者提供了丰富的网络通信和游戏逻辑处理功能,是深入理解网络游戏开发技术的重要参考资料。本文将从...

Global site tag (gtag.js) - Google Analytics