JDBC使用PrepareStatement对性能的提升分析

下文均基于mysql-connector-java-5.1.43, mysql server version 5.6版本进行分析。

从刚开始接触JDBC开始,就学到使用PrepareStatement对sql进行预编译,不用每次语句都进行一次重新sql解析和编译,相较于使用Statement能够提高程序的性能,那么到底是用PrepareStatement对性能的提升有多大呢?

通过示例代码:

import java.sql.*;

/**
 * Created by ZHUKE on 2017/8/18.
 */
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1/test", "root", "root");
        String prepareSql = "select * from user_info where firstName = ?";
        PreparedStatement preparedStatement = conn.prepareStatement(prepareSql);

        Statement statement = conn.createStatement();
        String statementSql = "select * from user_info where firstName= 'zhuke'";

        long nowTime = System.currentTimeMillis();

        int count = 100000;
        for (int i = 0; i < count; i++) {
            preparedStatement.setString(1, "zhuke");
            preparedStatement.execute();
        }
        long nowTime1 = System.currentTimeMillis();
        System.out.println("preparedStatement execute " + count + " times consume " + (nowTime1 - nowTime) + " ms");

        long nowTime2 = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            statement.execute(statementSql);
        }
        long nowTime3 = System.currentTimeMillis();
        System.out.println("statement execute " + count + " times consume " + (nowTime3 - nowTime2) + " ms");

    }
}

执行同样的语句100000次,得到的结果如下:

测试结果

14588 : 14477,这就是我一直深信的性能提升???

一定是哪里出了问题,通过查找资料知道,PrepareStatement会将带有参数占位符?的sql语句提交到mysql服务器,服务器会对sql语句进行解析和编译,将编译后的sql id返回给客户端,客户端下次值需要将参数值和sql id发送到服务器即可。以此节省了服务器多次重复编译同一sql语句的开销,而且因为不用每次都发送完整sql内容,也一定程度上节省了网络开销。

那么为什么以上代码中,PrepareStatement没有实现性能提升呢?
通过开启mysql的详细日志,对PrepareStatement的执行来一探究竟。

preparedStatement.setString(1, "zhuke");
preparedStatement.execute();

mysql日志如下:

PrepareStatement执行mysql日志

通过mysql日志我们可以看到,通过PrepareStatement的方式,每次执行发送给mysql服务器的依然是完整的参数拼接完成后的sql语句,并没有利用到上述的服务器预编译的特性。

通过mysql-connector-java(5.1.43版本)连接驱动的源码来查找原因。

public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
    synchronized (getConnectionMutex()) {
        ……

        if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
            canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
        }
        //如果useServerPreparedStmts配置为true,且服务器支持sql预编译优化,则执行服务器sql优化
        if (this.useServerPreparedStmts && canServerPrepare) {
            if (this.getCachePreparedStatements()) {
                synchronized (this.serverSideStatementCache) {
                    ……
        } else {//否则执行本地预编译
            ……
        }

        return pStmt;
    }
}

服务器支持预编译的情况下,那么就只由useServerPreparedStmts 控制是否进行服务器预编译了。而从源码中又知道其默认值为false。那么如果不显式配置useServerPreparedStmts =true,就不会进行服务器预编译,而只执行本地预编译。

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).
通过查找MySQL官网发现,驱动文件在版本 5.0.5后将设为了false,所以需要手动指定和开启服务器预编译功能。
https://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-0-5.html

通过在url链接中添加参数useServerPreparedStmts =true开启服务器预编译。
现在我们看到mysql日志信息如下:

useServerPreparedStmts =true时mysql日志信息

此时我们看到,开启了服务器预编译后,mysql服务器会首先prepare
预编译

select * from user_info where firstName = ?

语句。

再次实验以上代码,看看性能提升了多少:

开启useServerPreparedStmts 后执行结果

13312 : 14535,性能提升了8.4%.

与之对应的还有一个参数:cachePrepStmts表示服务器是否需要缓存prepare预编译对象。

// 关闭cachePrepStmts时新建两个preparedStatement 
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1/test?useServerPrepStmts=true", "root", "root");
String prepareSql = "select * from user_info where firstName = ?";
PreparedStatement preparedStatement = conn.prepareStatement(prepareSql);

preparedStatement.setString(1, "zhuke");
preparedStatement.execute();
preparedStatement.close();

preparedStatement = conn.prepareStatement(prepareSql);
preparedStatement.setString(1, "zhuke1");
preparedStatement.execute();
preparedStatement.close();
关闭cachePrepStmts时新建两个preparedStatement

可以看到此时,针对完全相同的sql语句,服务器进行了两次预编译过程。

那么当我们开启cachePrepStmts的时候呢?

// 关闭cachePrepStmts时新建两个preparedStatement 
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1/test?useServerPrepStmts=true&cachePrepStmts=true", "root", "root");
String prepareSql = "select * from user_info where firstName = ?";
PreparedStatement preparedStatement = conn.prepareStatement(prepareSql);

preparedStatement.setString(1, "zhuke");
preparedStatement.execute();
preparedStatement.close();

preparedStatement = conn.prepareStatement(prepareSql);
preparedStatement.setString(1, "zhuke1");
preparedStatement.execute();
preparedStatement.close();
开启开启cachePrepStmts时的mysql日志

可以看到,开启cachePrepStmts时,mysql服务器只进行了一次预编译过程。

通过阅读源码发现,当开启cachePrepStmts时,客户端会以sql语句作为键,预编译完成后的对象PrepareStatement作为值,保存在Map中,以便下次可以重复利用和缓存。

//prepareStatement关闭时,将对象存入缓存中
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;
            }

            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) {
                        ((ServerPreparedStatement) oldServerPrepStmt).isCached = false;
                        ((ServerPreparedStatement) oldServerPrepStmt).realClose(true, true);
                    }
                }
            }
        }
    }


结论

使用mysql的预编译对象PrepateStatement时,一定需要设置useServerPrepStmts=true开启服务器预编译功能,设置cachePrepStmts=true开启客户端对预编译对象的缓存。

参考资料:
https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html
http://www.cnblogs.com/justfortaste/p/3920140.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354

推荐阅读更多精彩内容