mysql预编译-Prepared-Statements

前几天在和我弟聊到数据预编译的时候突然想到预编译为什么可以加快访问速度?我们一般在jdbc连接的时候sql用占位符就认为是使用了数据库的预编译功能,但实际又是怎么样的呢?然后自己网上查了一些文章,这里也做一些自己的总结。

数据库基本配置

这里先对数据库进行基本配置以保证后续测试正常运行。

  • 开启数据库日志记录
     --- 查看配置
    show variables like 'general%';
    +------------------+--------------------------------------------+
    | Variable_name    | Value                                      |
    |------------------+--------------------------------------------|
    | general_log      | ON                                         |
    | general_log_file | /var/lib/mysql/iZwz9ecwpuisr9ww016vuyZ.log |
    +------------------+--------------------------------------------+

    -- 设置开启
    set global general_log = on;   
  • 创建测试表

    CREATE TABLE
    t_test(
        id INT PRIMARY KEY,
        value VARCHAR(20)
    );

使用MySql客户端连接进行预编译

编译sql语句

  • 语法

    PREPARE stmt_name FROM preparable_stm
  • 编译语句

    prepare ins from 'insert into t_test select ?, ?';

执行

-- 声明变量
set @id=1,@value='abc';

-- 执行语句
execute ins USING @id,@value;

执行日志

  • 执行语句

    prepare sel1 from 'select * from t_test where value=?';
    set @value1='abc';
    execute sel1 USING @value1;
  • 日志

    2018-06-04T07:40:06.827917Z       413 Query     PREPARE sel1 FROM ...
    2018-06-04T07:40:06.828062Z       413 Prepare   select * from t_test where value=?
    2018-06-04T07:40:35.810805Z       413 Query     set @value1='abc'
    2018-06-04T07:40:56.044515Z       413 Query     execute sel1 USING @value1
    2018-06-04T07:40:56.044587Z       413 Execute   select * from t_test where value='abc'

使用MySql数据java驱动进行预编译

jdbc插入数据

public static void main(String[] args) throws ClassNotFoundException {
    Class.forName("com.mysql.jdbc.Driver");

    String url = "jdbc:mysql://119.23.71.218/test";
    try(Connection con = DriverManager.getConnection(url, "root", "cc")) {
        String sql = "insert into t_test select ?, ?";
        PreparedStatement statement = con.prepareStatement(sql);
        statement.setInt(1, 11);
        statement.setString(2, "abc");
        statement.executeUpdate();
        statement.close();

        String sql1 = "insert into t_test select ?, ?";
        PreparedStatement statement1 = con.prepareStatement(sql1);
        statement1.setInt(1, 12);
        statement1.setString(2, "abc");
        statement1.executeUpdate();
        statement1.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

日志

2018-06-04T09:31:47.827778Z   421 Connect   [email protected] on test using TCP/IP
2018-06-04T09:31:48.829052Z   421 Query /* mysql-connector-java-5.1.20 ( Revision: [email protected] ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
2018-06-04T09:31:49.828078Z   421 Query /* mysql-connector-java-5.1.20 ( Revision: [email protected] ) */SELECT @@session.auto_increment_increment
2018-06-04T09:31:50.827721Z   421 Query SHOW COLLATION
2018-06-04T09:31:53.827691Z   421 Query SHOW CHARACTER SET
2018-06-04T09:31:54.827731Z   421 Query SET NAMES latin1
2018-06-04T09:31:55.835948Z   421 Query SET character_set_results = NULL
2018-06-04T09:31:56.828836Z   421 Query SET autocommit=1
2018-06-04T09:31:57.827763Z   421 Query insert into t_test select 11, 'abc'
2018-06-04T09:31:58.827873Z   421 Query insert into t_test select 12, 'abc'
2018-06-04T09:31:59.807905Z   421 Quit  

从日志中可以看出,这时没有使用和执行预编译。客户端直接生成sql执行。

开启服务端预编译

  • jdbc连结url中添加useServerPrepStmts=true,启用服务端预编译

    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.mysql.jdbc.Driver");
    
        String url = "jdbc:mysql://119.23.71.218/test?useServerPrepStmts=true";
        try(Connection con = DriverManager.getConnection(url, "root", "cc")) {
            String sql = "insert into t_test select ?, ?";
            PreparedStatement statement = con.prepareStatement(sql);
    
            statement.setInt(1, 4);
            statement.setString(2, "abc");
            statement.executeUpdate();
    
            statement.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
  • 日志

    2018-06-04T08:57:56.828142Z   416 Connect   [email protected] on test using TCP/IP
    2018-06-04T08:57:56.861148Z   416 Query /* mysql-connector-java-5.1.20 ( Revision: [email protected] ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
    2018-06-04T08:57:56.911249Z   416 Query /* mysql-connector-java-5.1.20 ( Revision: [email protected] ) */SELECT @@session.auto_increment_increment
    2018-06-04T08:57:56.939101Z   416 Query SHOW COLLATION
    2018-06-04T08:57:56.974224Z   416 Query SHOW CHARACTER SET
    2018-06-04T08:57:57.004165Z   416 Query SET NAMES latin1
    2018-06-04T08:57:57.031271Z   416 Query SET character_set_results = NULL
    2018-06-04T08:57:57.059271Z   416 Query SET autocommit=1
    2018-06-04T08:57:57.101266Z   416 Prepare   insert into t_test select ?, ?
    2018-06-04T08:57:57.811185Z   416 Execute   insert into t_test select 4, 'abc'
    2018-06-04T08:57:57.841198Z   416 Close stmt    
    2018-06-04T08:57:57.841266Z   416 Quit  

    可以看到这是预编译了语句,并且执行了语句。

  • 启用服务端预编译缓存,jdbc连接url增加cachePrepStmts=true
    未添加缓存的测试:

public static void main(String[] args) throws ClassNotFoundException {
    Class.forName("com.mysql.jdbc.Driver");

    String url = "jdbc:mysql://119.23.71.218/test?useServerPrepStmts=true";
    try(Connection con = DriverManager.getConnection(url, "root", "cc")) {
        String sql = "insert into t_test select ?, ?";
        PreparedStatement statement = con.prepareStatement(sql);
        statement.setInt(1, 5);
        statement.setString(2, "abc");
        statement.executeUpdate();
        statement.close();

        String sql1 = "insert into t_test select ?, ?";
        PreparedStatement statement1 = con.prepareStatement(sql1);
        statement1.setInt(1, 6);
        statement1.setString(2, "abc");
        statement1.executeUpdate();
        statement1.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

日志:

2018-06-04T09:09:33.828016Z   417 Connect   [email protected] on test using TCP/IP
2018-06-04T09:09:33.851918Z   417 Query /* mysql-connector-java-5.1.20 ( Revision: [email protected] ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
2018-06-04T09:09:33.896901Z   417 Query /* mysql-connector-java-5.1.20 ( Revision: [email protected] ) */SELECT @@session.auto_increment_increment
2018-06-04T09:09:33.917895Z   417 Query SHOW COLLATION
2018-06-04T09:09:33.945729Z   417 Query SHOW CHARACTER SET
2018-06-04T09:09:33.966685Z   417 Query SET NAMES latin1
2018-06-04T09:09:33.985769Z   417 Query SET character_set_results = NULL
2018-06-04T09:09:34.004857Z   417 Query SET autocommit=1
2018-06-04T09:09:34.040981Z   417 Prepare   insert into t_test select ?, ?
2018-06-04T09:09:34.060854Z   417 Execute   insert into t_test select 5, 'abc'
2018-06-04T09:09:34.084805Z   417 Close stmt    
2018-06-04T09:09:34.085837Z   417 Prepare   insert into t_test select ?, ?
2018-06-04T09:09:34.103853Z   417 Execute   insert into t_test select 6, 'abc'
2018-06-04T09:09:34.124749Z   417 Close stmt    
2018-06-04T09:09:34.124928Z   417 Quit  

从日志中可以看到这个时候虽然预编译执行发送到了mysql服务端,但是两个一样的sql会编译两次,这个时候就需要增加预编译缓存了(注意:我们这里java连接时关闭了stmt,或是重新建立了一个stmt)。

启用预编译缓存,修改jdbcurl配置:

public static void main(String[] args) throws ClassNotFoundException {
    Class.forName("com.mysql.jdbc.Driver");

    String url = "jdbc:mysql://119.23.71.218/test?useServerPrepStmts=true&cachePrepStmts=true";
    try(Connection con = DriverManager.getConnection(url, "root", "cc")) {
        String sql = "insert into t_test select ?, ?";
        PreparedStatement statement = con.prepareStatement(sql);
        statement.setInt(1, 9);
        statement.setString(2, "abc");
        statement.executeUpdate();
        statement.close();

        String sql1 = "insert into t_test select ?, ?";
        PreparedStatement statement1 = con.prepareStatement(sql1);
        statement1.setInt(1, 10);
        statement1.setString(2, "abc");
        statement1.executeUpdate();
        statement1.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

日志:

2018-06-04T09:17:27.944791Z   419 Connect   [email protected] on test using TCP/IP
2018-06-04T09:17:27.968875Z   419 Query /* mysql-connector-java-5.1.20 ( Revision: [email protected] ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
2018-06-04T09:17:28.012936Z   419 Query /* mysql-connector-java-5.1.20 ( Revision: [email protected] ) */SELECT @@session.auto_increment_increment
2018-06-04T09:17:28.032769Z   419 Query SHOW COLLATION
2018-06-04T09:17:28.060912Z   419 Query SHOW CHARACTER SET
2018-06-04T09:17:28.081732Z   419 Query SET NAMES latin1
2018-06-04T09:17:28.103845Z   419 Query SET character_set_results = NULL
2018-06-04T09:17:28.125928Z   419 Query SET autocommit=1
2018-06-04T09:17:28.165973Z   419 Prepare   insert into t_test select ?, ?
2018-06-04T09:17:28.188790Z   419 Execute   insert into t_test select 9, 'abc'
2018-06-04T09:17:28.235868Z   419 Execute   insert into t_test select 10, 'abc'
2018-06-04T09:17:28.258873Z   419 Quit

这个时候我们查看日志可能看到,预编译sql只执行了一次。

本地预编译+缓存

sql注入相关

注入说明

实例:

select * from t_test where id=1 or 1=1;

使用mysql客户端

-- 正常查询
prepare sel from 'select * from t_test where value = ?';
set @v1='abc';
execute sel using @v1;
-- 注入
set @v1='abc' or 1=1;
execute sel using @v1;
-- 后台查询查询语句变成 select * from t_test where value = 1

java客户端连接

  • 未开启预编译

    String sql2 = "select * from t_test where value = ?";
    PreparedStatement statement2 = con.prepareStatement(sql2);
    statement2.setString(1, "'abc' or 1=1");
    ResultSet resultSet = statement2.executeQuery();
    resultSet.next();
    System.out.println(resultSet.getInt(1));
    System.out.println(resultSet.getString(2));

    执行日志查看:

    2018-06-04T10:30:12.253397Z   444 Query select * from t_test where value = 'abc'
  • 开启预编译
    执行日志查看:

    2018-06-04T10:31:19.273094Z   445 Prepare   select * from t_test where value = ?
    2018-06-04T10:31:19.295910Z   445 Execute   select * from t_test where value = '\'abc\' or 1=1'

参考文章

参考1

猜你喜欢

转载自www.cnblogs.com/liuchengcc/p/9134879.html