前几天在和我弟聊到数据预编译的时候突然想到预编译为什么可以加快访问速度?我们一般在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'