你不知道的PreparedStatement预编译[通俗易懂]

你不知道的PreparedStatement预编译[通俗易懂]大家都知道,Mybatis内置参数,形如#{xxx}的,均采用了sql预编译的形式,大致知道mybatis底层使用PreparedStatement,过程是先将带有占位符(即”?”)的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,然后直接执行带有真实参数的sql。如果你的基本结论也是如此,那你就大错特错了。目录1.mysql是否默认开启了预编译功…

大家好,又见面了,我是你们的朋友全栈君。

大家都知道,Mybatis内置参数,形如#{xxx}的,均采用了sql预编译的形式,大致知道mybatis底层使用PreparedStatement,过程是先将带有占位符(即”?”)的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,然后直接执行带有真实参数的sql。如果你的基本结论也是如此,那你就大错特错了。

目录

1. mysql是否默认开启了预编译功能?

2. 预编译缓存是服务端还是客户端缓存?

3. 开启预编译性能更高?

4. 从源码中验证

5. 总结


1. mysql是否默认开启了预编译功能?

mysql是否支持预编译有两层意思:

  1. db是否支持预编译
  2.  连接数据库的url是否指定了需要预编译,比如:jdbc:mysql://127.0.0.1:3306/user?useServerPrepStmts=true,useServerPrepStmts=true是非常非常重要的参数。如果不配置PreparedStatement 实际是个假的 PreparedStatement
SELECT VERSION();   // 5.6.24-log

SHOW GLOBAL STATUS LIKE '%prepare%'; //Com_stmt_prepare 4  代表被执行预编译次数

//开启server日志
SHOW VARIABLES LIKE '%general_log%';
SHOW VARIABLES LIKE 'log_output';

SET GLOBAL general_log = ON;
SET GLOBAL log_output='table';

TRUNCATE TABLE mysql.general_log;    
SELECT * FROM mysql.general_log;  // 有Prepare命令

注意:mysql预编译功能有版本要求,包括server版本和mysql.jar包版本。以前的版本默认useServerPrepStmts=true,5.0.5以后的版本默认useServerPrepStmts=false

2. 预编译缓存是服务端还是客户端缓存?

开启缓存:useServerPrepStmts=true&cachePrepStmts=true,设置了useServerPrepStmts=true,虽然可以一次编译,多次执行

它可以提高性能,但缓存是针对连接的,即每个连接的缓存都是独立的,并且缓存主要是由mysql-connector-java.jar实现的。

当手动调用prepareStatement.close()时PrepareStatement对象只会将关闭状态置为关闭,并不会向mysql发送关闭请求,prepareStatement对象会被缓存起来,等下次使用的时候直接从缓存中取出来使用。没有开启缓存,则会向mysql发送closeStmt的请求。

3. 开启预编译性能更高?

也就是说预编译比非预编译更好?其实不然,不行自己可试试看。

public class PreparedStatement_test {
    private String url = "jdbc:mysql://localhost:3306/batch";
    private String sql = "SELECT * FROM export_request WHERE id = ?";
    private int maxTimes = 100000;

    @Test
    public void go_driver() throws SQLException, ClassNotFoundException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = (Connection) DriverManager.getConnection(url, "root", "123456");
        // PreparedStatement
        Stopwatch stopwatch = Stopwatch.createStarted();
        for (int i = 0; i < maxTimes; i++) {
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setLong(1, Math.abs(new Random().nextLong()));
            // execute
            stmt.executeQuery();
        }
        System.out.println("go_driver:" + stopwatch);
    }

    @Test
    public void go_setPre() throws SQLException, ClassNotFoundException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = (Connection) DriverManager.getConnection(url + "?useServerPrepStmts=true", "root", "123456");
        // PreparedStatement
        Stopwatch stopwatch = Stopwatch.createStarted();
        for (int i = 0; i < maxTimes; i++) {
            PreparedStatement stmt = conn.prepareStatement(sql);
            stmt.setLong(1, Math.abs(new Random().nextLong()));
            // execute
            stmt.executeQuery();
        }
        System.out.println("go_setPre:" + stopwatch);
    }

    @Test
    public void go_setPreCache() throws SQLException, ClassNotFoundException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = (Connection) DriverManager.getConnection(url + "?useServerPrepStmts=true&cachePrepStmts=true", "root", "123456");
        // PreparedStatement
        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.setLong(1, Math.abs(new Random().nextLong()));
        // execute
        stmt.executeQuery();
        stmt.close();//非常重要的,一定要调用才会缓存
        Stopwatch stopwatch = Stopwatch.createStarted();
        for (int i = 0; i < maxTimes; i++) {
            stmt = conn.prepareStatement(sql);
            stmt.setLong(1, Math.abs(new Random().nextLong()));
            // execute
            stmt.executeQuery();
        }
        System.out.println("go_setPreCache:" + stopwatch);
    }
}

基准为10w次单线程:
非预编译::23.78 s
预编译:41.86 s
预编译缓存:20.55 s

经过实践测试,对于频繁适用的语句,使用预编译+缓存确实能够得到可观的提升,但对于不频繁适用的语句,服务端编译会增加额外的round-trip。开发实践中要视情况而定。

4. 从源码中验证

预编译原理(connection -> prepareStatement )

预编译:JDBC42ServerPreparedStatement(需将对应占位符)

非预编译:JDBC42PreparedStatement(完整的SQL)

//com.mysql.jdbc.ConnectionImpl中的代码片段
  /**
     * JDBC 2.0 Same as prepareStatement() above, but allows the default result
     * set type and result set concurrency type to be overridden.
     * 
     * @param sql
     *            the SQL query containing place holders
     * @param resultSetType
     *            a result set type, see ResultSet.TYPE_XXX
     * @param resultSetConcurrency
     *            a concurrency type, see ResultSet.CONCUR_XXX
     * @return a new PreparedStatement object containing the pre-compiled SQL
     *         statement
     * @exception SQLException
     *                if a database-access error occurs.
     */
    public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        synchronized (getConnectionMutex()) {
            checkClosed();
            //
            // FIXME: Create warnings if can't create results of the given type or concurrency
            //当Client开启 useServerPreparedStmts 并且Server支持 ServerPrepare
            PreparedStatement pStmt = null;
            boolean canServerPrepare = true;
            String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql) : sql;
            if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
                canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
            }
            
            if (this.useServerPreparedStmts && canServerPrepare) {// 从缓存中获取 pStmt
                if (this.getCachePreparedStatements()) {
                    synchronized (this.serverSideStatementCache) {
                        pStmt = (com.mysql.jdbc.ServerPreparedStatement) this.serverSideStatementCache
                                .remove(makePreparedStatementCacheKey(this.database, sql));

                        if (pStmt != null) {
                            ((com.mysql.jdbc.ServerPreparedStatement) pStmt).setClosed(false);
                            pStmt.clearParameters();// 清理上次留下的参数
                        }

                        if (pStmt == null) {
                            try {// 向Server提交 SQL 预编译,实例是JDBC42ServerPreparedStatement
                                pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType,
                                        resultSetConcurrency);
                                if (sql.length() < getPreparedStatementCacheSqlLimit()) {
                                    ((com.mysql.jdbc.ServerPreparedStatement) pStmt).isCached = true;
                                }

                                pStmt.setResultSetType(resultSetType);
                                pStmt.setResultSetConcurrency(resultSetConcurrency);
                            } catch (SQLException sqlEx) {
                                // Punt, if necessary
                                if (getEmulateUnsupportedPstmts()) {
                                    pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);

                                    if (sql.length() < getPreparedStatementCacheSqlLimit()) {
                                        this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);
                                    }
                                } else {
                                    throw sqlEx;
                                }
                            }
                        }
                    }
                } else {
                    try {    // 向Server提交 SQL 预编译。
                        pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);

                        pStmt.setResultSetType(resultSetType);
                        pStmt.setResultSetConcurrency(resultSetConcurrency);
                    } catch (SQLException sqlEx) {
                        // Punt, if necessary
                        if (getEmulateUnsupportedPstmts()) {
                            pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
                        } else {
                            throw sqlEx;
                        }
                    }
                }
            } else {// Server不支持 ServerPrepare,实例是JDBC42PreparedStatement
                pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
            }
            return pStmt;
        }
    }

JDBC42ServerPreparedStatement->close,缓存你不知道的PreparedStatement预编译[通俗易懂]

//com.mysql.jdbc.ServerPreparedStatement中选取代码
@Override
public void close() throws SQLException {
    MySQLConnection locallyScopedConn = this.connection;

    if (locallyScopedConn == null) {
        return; // already closed
    }

    synchronized (locallyScopedConn.getConnectionMutex()) {
        if (this.isCached && isPoolable() && !this.isClosed) {
            clearParameters();// 若开启缓存,则只会将状态位设为已关闭,并且刷新缓存
            this.isClosed = true;
            this.connection.recachePreparedStatement(this);
            return;
        }
        //没有开启缓存,则会向mysql发送closeStmt的请求
        realClose(true, true);
    }
}
 public void recachePreparedStatement(ServerPreparedStatement pstmt) throws SQLException {
        synchronized (getConnectionMutex()) {
            if (getCachePreparedStatements() && pstmt.isPoolable()) {
                synchronized (this.serverSideStatementCache) {
                    Object oldServerPrepStmt = this.serverSideStatementCache.put(makePreparedStatementCacheKey(pstmt.currentCatalog, pstmt.originalSql), pstmt);
                    if (oldServerPrepStmt != null) {// 将sql语句作为key,reparedStatement对象作为value存放到缓存中
                        ((ServerPreparedStatement) oldServerPrepStmt).isCached = false;
                        ((ServerPreparedStatement) oldServerPrepStmt).realClose(true, true);
                    }
                }
            }
        }
    }

5. 总结

  1. 预编译显式开启(在url中指定useServerPrepStmts=true),否则PreparedStatement不会向mysql发送预编译(Prepare命令)的请求;
  2. 每次向mysql发送预编译请求,不管之前有没有执行过此SQL语句,只要请求的命令是Prepare或Query,mysql就会重新编译一次SQL语句,并返回此链接当前唯一的Statement ID,后续执行SQL语句的时候,程序只需拿着Statement ID和参数就可以了;
  3. 当预编译的SQL语句有语法错误,则mysql的响应会携带错误信息,但此错误信息JDBC感知不到(或者说mysql-connetor-java.jar包里的实现将其忽略掉了),此时还会继续往下执行代码,当执行到executeXxx()方法时,由于没有Statement ID(所以就会将拼接完整的SQL语句值已经将占位符(?)替换掉再次发给mysql请求执行,此时mysql响应有语法错误,这时JDBC就会抛出语法错误异常),所以检查语法那一步实在mysql-server中做的(通过抓包可以看到);
  4. PreparedStatement对性能的提高是利用缓存实现的,需要显式开启(在url中指定cachePrepStmts=true),此缓存是mysql-connetor-java.jar包里实现的(非mysql-server中的缓存),缓存的key是完整的sql语句,value是PreparedStatement对象。放入缓存是PreparedStatement.close()触发的,所以只要缓存PreparedStatement对象没有关闭,你不管调用多少次connection.prapareStatement(sql)对相同的sql语句进行预编译,都会将预编译的请求发给mysql,mysql也会对每一个sql语句不管是否相同进行预编译,并生成一个唯一的Statement ID并返回;
  5. 缓存是针对链接的,每个链接都是独立的,不共享缓存 
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/138800.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • mysql 环境_MySQL怎么配置环境变量?「建议收藏」

    mysql 环境_MySQL怎么配置环境变量?「建议收藏」安装完MySQL后,如果不配置环境变量的话,每次还要转到mysql/bin目录下才能操作,下面本篇文章就来给大家介绍一下如何配置环境变量,希望对大家有所帮助。MySQL配置环境变量的步骤:1、右键【我的电脑】,选择【属性】2、选择左侧的【高级系统设置】3、在弹出的窗口点击右下角【环境变量】4、点击新建,在弹出窗口变量名输入mysql_home,变量值输入你的mysql安装路径,如图:5、编辑Pat…

    2022年6月18日
    33
  • 主流流媒体服务器软件,十款免费的流媒体服务器软件介绍

    主流流媒体服务器软件,十款免费的流媒体服务器软件介绍互联网时代,服务器是网络的重要支撑,大家租用云服务器除了搭建网站服务器之外,还会用到搭建其他各种WEB应用服务器,而流媒体服务器的搭建就是其中一种,那么应该怎么进行流媒体服务器的搭建呢?你知道有那些免费的流媒体服务器软件吗?(你可能想知道:视频流媒体服务器的选择方式?)流媒体服务器是指提供以流方式在网络中传送音频、视频和多媒体文件的媒体形式服务的服务器。它的主要功能是流式协议(RTP/RTSP、M…

    2022年6月13日
    45
  • 2022.01.4 idea激活码【2022.01最新】2022.02.04

    (2022.01.4 idea激活码)本文适用于JetBrains家族所有ide,包括IntelliJidea,phpstorm,webstorm,pycharm,datagrip等。IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html…

    2022年4月1日
    228
  • Python 编译器_如何在pe系统里安装软件

    Python 编译器_如何在pe系统里安装软件好久都没更新博客了,最近是真的很忙,每天抽出1小时写博客,有的时候更本没时间,今天写一个解析PE的一个软件,过程和内容很干,干货干货之前有很多人加我要资料和软件,我从来没说过要钱什么的,只要给个关注和点赞,就可以了,需要什么资料,只要我可以给,我会不要一分钱免费给你们资料,欢迎大家来评论博主?点个赞留个关注吧!!资料(百度网盘)提取码:i4ptPE解析软件和源代码包文件提取码:07bhPE解析器软件安装包提取码:r9og激活成功教程版打包软件–打包为安装包先看视频,双击打开

    2022年10月16日
    3
  • [哎]关于ftp扫描工具的激活成功教程问题[通俗易懂]

    [哎]关于ftp扫描工具的激活成功教程问题[通俗易懂]先前发布过一个工具,用于ftp弱口令扫描 文章地址:http://blog.csdn.net/prsniper/article/details/6101770 当时为了吸引一些反汇编方面的高手交流,故意把DLL使用期限限制在2010年,可惜没人鸟我~~~~~~~~~

    2022年10月1日
    4
  • AWVS简单操作[通俗易懂]

    AWVS简单操作[通俗易懂]AWVS简单介绍AcunetixWebVulnerabilityScanner(简称AWVS)是一款知名的网络漏洞扫描工具,它通过网络爬虫检查SQL注入攻击漏洞、XSS跨站脚本攻击漏洞等漏洞检测流行安全漏洞,来审核Web应用程序的安全性。但有些漏洞,还是扫不出来的,比如:逻辑漏洞、一些较隐蔽的XSS和SQL注入。所以,渗透的时候,工具一般都是需要人员来配合使用的。AWVS官方网站是:…

    2022年9月22日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号