1、背景java
MySQL是一个中小型关系型数据库管理系统,目前咱们淘宝也使用的也很是普遍。为了对开发中间DAO持久层的问题能有更深的理解以及最近在使用的phoenix on Hbase的SQL也是实现的JDBC规范,在遇到问题的时候可以有更多的思路,因而研究了一下MySQL_JDBC驱动的源码,你们都知道JDBC是Java访问数据库的一套规范,具体访问数据库的细节有各个数据库厂商本身实现,看驱动实现也有助有咱们更好的理解JDBC规范,而且在这过程当中也发现了一直以来对于PreparedStatement常识理解上的错误,与你们分享(MySQl版本5.1.39,JDBC驱动版本5.1.7,JDK版本1.6)。 mysql
2、JDBC典型应用
下面是个最简单的使用JDBC取得数据的应用。主要能分红几个步骤,分别是①加载数据库驱动,②获取数据库链接,③建立PreparedStatement,而且设置参数 ④ 执行查询 ,来分步分析这个过程。基本上每一个步骤的源码分析我都画了时序图,若是不想看文字的话,能够对着时序图看。最后我还会分析关于PreparedStatement预编译的话题,有兴趣的同窗能够仔细看下。sql
Java代码 数据库
1. public class PreparedStatement_Select { 编程
2. private Connection conn = null; 数组
3. private PreparedStatement pstmt = null; 缓存
4. private ResultSet rs = null; 服务器
5. private String sql = "SELECT * FROM user WHERE id = ?";网络
7. public void selectStudent(int id) { 框架
8. try {
9. // step1:加载数据库厂商提供的驱动程序
10. Class.forName(“ com.mysql.jdbc.Driver ”);
11. } catch (ClassNotFoundException e) {
12. e.printStackTrace();
13. }
15. String url = "jdbc:mysql://localhost:3306/studb";
16. try {
17. // step2:提供数据库链接的URL,经过DriverManager得到数据库的一个链接对象
18. conn = DriverManager.getConnection(url, "root", "root");
19. } catch (SQLException e) {
20. e.printStackTrace();
21. }
23. try {
24. // step3:建立Statement(SQL的执行环境)
25. pstmt = conn.prepareStatement(sql);
26. pstmt.setInt(1, id);
28. // step4: 执行SQL语句
29. rs = pstmt.executeQuery();
31. // step5: 处理结果
32. while (rs.next()) {
33. int i = 1;
34. System.out.print(" 学员编号: " + rs.getInt(i++));
35. System.out.print(", 学员用户名: " + rs.getString(i++));
36. System.out.print(", 学员密码: " + rs.getString(i++));
37. System.out.println(", 学员年龄: " + rs.getInt(i++));
38. }
39. } catch (SQLException e) {
40. e.printStackTrace();
41. } finally {
42. // step6: 关闭数据库链接
43. DbClose.close(rs, pstmt, conn);
44. }
45. }
46.}
3、JDBC驱动源码解析
Java数据库链接(JDBC)由一组用 Java 编程语言编写的类和接口组成。JDBC 为工具/数据库开发人员提供了一个标准的 API,使他们可以用纯Java API 来编写数据库应用程序。说白了一套Java访问数据库的统一规范,以下图,具体与数据库交互的仍是由驱动实现,JDBC规范之于驱动的关系,也相似于Servlet规范与Servlet容器(Tomcat)的关系,本质就是一套接口和一套实现的关系。以下类图所示,咱们平时开发JDBC时熟悉的Connection接口在Mysql驱动中的实现类是com.mysql.jdbc.JDBC4Connection类,PreparedStatement接口在Mysql驱动中的实现类是com.mysql.jdbc.JDBC4Connection, ResultSet接口在Mysql驱动中的实现类是 com.mysql.jdbc.JDBC4ResultSet,下面的源码解析也是经过这几个类展开。
1:加载数据库厂商提供的驱动程序
首先咱们经过Class.forName("com.mysql.jdbc.Driver")来加载mysql的jdbc驱动。 Mysql的com.mysql.jdbc.Driver类实现了java.sql.Driver接口,任何数据库提供商的驱动类都必须实现这个接口。在DriverManager类中使用的都是接口Driver类型的驱动,也就是说驱动的使用不依赖于具体的实现,这无疑给咱们的使用带来很大的方便。若是须要换用其余的数据库的话,只须要把Class.forName()中的参数换掉就能够了,能够说是很是方便的,com.mysql.jdbc.Driver类也是驱动实现JDBC规范的第一步。
Java代码
1. public class Driver extends NonRegisteringDriver implements java.sql.Driver {
2. static {
3. try {
4. //往DriverManager中注册自身驱动
5. java.sql.DriverManager.registerDriver(new Driver());
6. } catch (SQLException E) {
7. throw new RuntimeException("Can't register driver!");
8. }
9. }
10. public Driver() throws SQLException {
11. }
12. }
在com.mysql.jdbc.Driver类的静态初始化块中会向java.sql.DriverManager注册它本身 ,注册驱动首先就是初始化,而后把驱动的信息封装一下放进一个叫作DriverInfo的驱动信息类中,最后放入一个驱动的集合中, 到此Mysql的驱动类com.mysql.jdbc.Driver也就已经注册到DriverManager中了。
Java代码
1. public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
2. if (!initialized) {
3. initialize();
4. }
6. DriverInfo di = new DriverInfo();
8. //把driver的信息封装一下,组成一个DriverInfo对象
9. di.driver = driver;
10. di.driverClass = driver.getClass();
11. di.driverClassName = di.driverClass.getName();
13. writeDrivers.addElement(di);
14. println("registerDriver: " + di);
16. readDrivers = (java.util.Vector) writeDrivers.clone();
17. }
注册驱动的具体过程序列图以下:
2.获取数据库链接
数据库链接的本质其实就是客户端维持了一个和远程MySQL服务器的一个TCP长链接,而且在此链接上维护了一些信息。
经过 DriverManager.getConnection(url, "root", "root")获取数据库链接对象时,因为以前已经在 DriverManager中注册了驱动类 ,全部会找到那个驱动类来链接数据库com.mysql.jdbc.Driver.connect
Java代码
1. private static Connection getConnection(
2. String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
3. java.util.Vector drivers = null;
5. if (!initialized) {
6. initialize();
7. }
8. //取得链接使用的driver从readDrivers中取
9. synchronized (DriverManager.class){
10. drivers = readDrivers;
11. }
13. SQLException reason = null;
14. for (int i = 0; i < drivers.size(); i++) {
15. DriverInfo di = (DriverInfo)drivers.elementAt(i);
17. if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
18. continue;
19. }
20. try {
21. // 找到可供使用的驱动,链接数据库server
22. Connection result = di.driver.connect(url, info);
23. if (result != null) {
24. return (result);
25. }
26. } catch (SQLException ex) {
27. if (reason == null) {
28. reason = ex;
29. }
}
接着看com.mysql.jdbc.Driver.connect是如何创建链接返回数据库链接对象的, 写法很简洁,就是建立了一个MySQL的数据库链接对象, 传入host, port, database等链接信息,在com.mysql.jdbc.Connection的构造方法里面有个createNewIO()方法,主要会作两件事情,1、创建和MysqlServer的Socket链接,2、链接成功后,进行登陆校验, 发送用户名、密码、当前数据库链接默认选择的数据库名。
Java代码
1. public java.sql.Connection connect(String url, Properties info)
2. throws SQLException {
3. Properties props = null;
4. try {
5. // 传入host,port,database等链接信息建立数据库链接对象
6. Connection newConn = new com.mysql.jdbc.ConnectionImpl(host(props),
7. port(props), props, database(props), url);
9. return newConn;
10. } catch (SQLException sqlEx) {
11. throw sqlEx;
12. } catch (Exception ex) {
13. throw SQLError.createSQLException(...);
14. }
15. }
继续往下看ConnectionImpl构造器中的实现,会调用 createNewIO()方法来建立一个MysqlIO对象,维护在Connection中。
Java代码
1. protected ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info,
2. String databaseToConnectTo, String url)
3. throws SQLException {
4. try {
5. this.dbmd = getMetaData(false, false);
6. //建立MysqlIO对象,创建和MySql服务端的链接,而且进行登陆校验工做
7. createNewIO(false);
8. initializeStatementInterceptors();
9. this.io.setStatementInterceptors(this.statementInterceptors);
10. } catch (SQLException ex) {
11. cleanup(ex);
13. throw ex;
14. }
15. }
16. }
紧接着createNewIO()会建了一个com.mysql.jdbc.MysqlIO,利用com.mysql.jdbc.StandardSocketFactory来建立一个Socket创建与MySQL服务器的链接,而后就由这个mySqlIO来与MySql服务器进行握手(doHandshake()),这个doHandshake主要用来初始化与MySQL server的链接,负责登录服务器和处理链接错误。在其中会分析所链接的mysql server的版本,根据不一样的版本以及是否使用SSL加密数据都有不一样的处理方式,并把要传输给数据库server的数据都放在一个叫作packet的buffer中,调用send()方法往outputStream中写入要发送的数据。
Java代码
1. protected void createNewIO(boolean isForReconnect)
2. throws SQLException {
4. // 建立一个MysqlIO对象,创建与Mysql服务器的Socket链接
5. this.io = new MysqlIO(newHost, newPort, mergedProps,
6. getSocketFactoryClassName(), this,
7. getSocketTimeout(),
8. this.largeRowSizeThreshold.getValueAsInt());
10. // 登陆校验MySql Server, 发送用户名、密码、当前数据库链接默认选择的数据库名
11. this.io.doHandshake(this.user, this.password,
12. this.database);
14. // 获取MySql数据库链接的链接ID
15. this.connectionId = this.io.getThreadId();
16. this.isClosed = false;
17. }
具体的跟Mysql Server创建链接的代码以下:
Java代码
1. public MysqlIO(String host, int port, Properties props,
2. String socketFactoryClassName, ConnectionImpl conn,
3. int socketTimeout, int useBufferRowSizeThreshold) throws IOException, SQLException {
4. this.connection = conn;
6. try {
7. // 建立Socket对象,和MySql服务器创建链接
8. this.mysqlConnection = this.socketFactory.connect(this.host,
9. this.port, props);
11. // 获取Socket对象
12. this.mysqlConnection = this.socketFactory.beforeHandshake();
14. //封装SocketInputStream输入流
15. if (this.connection.getUseReadAheadInput()) {
16. this.mysqlInput = new ReadAheadInputStream(this.mysqlConnection.getInputStream(), 16384,
17. this.connection.getTraceProtocol(),
18. this.connection.getLog());
19. } else {
20. this.mysqlInput = new BufferedInputStream(this.mysqlConnection.getInputStream(),
21. 16384);
22. }
23. //封装ScoketOutputStream输出流
24. this.mysqlOutput = new BufferedOutputStream(this.mysqlConnection.getOutputStream(),
25. 16384);
26. }
具体的跟MySQL Server交互登陆校验的代码以下:
Java代码
1. void secureAuth411(Buffer packet, int packLength, String user,
2. String password, String database, boolean writeClientParams)
3. throws SQLException {
5. // 设置用户名
6. packet.writeString(user, "utf-8", this.connection);
8. if (password.length() != 0) {
9. packet.writeByte((byte) 0x14);
10. try {
11. // 设置密码
12. packet.writeBytesNoNull(Security.scramble411(password, this.seed, this.connection));
13. } catch (NoSuchAlgorithmException nse) {
14. }
16. if (this.useConnectWithDb) {
17. // 设置链接数据库名
18. packet.writeString(database, "utf-8", this.connection);
19. }
21. // 向Mysql服务器发送登陆信息包(用户名、密码、此Socket链接默认选择的数据库)
22. send(packet, packet.getPosition());
24. byte savePacketSequence = this.packetSequence++;
26. // 读取Mysql服务器登陆检验后发送的状态信息,若是成功就返回,若是登陆失败则抛出异常
27. Buffer reply = checkErrorPacket();
28. }
最终由SocketOutputStream通过一次RPC发送给MySQLServer进行验证。
Java代码
1. private final void send(Buffer packet, int packetLen)
2. throws SQLException {
3. try {
4. //把登陆信息的字节流发送给MySQL Server
5. this.mysqlOutput.write(packetToSend.getByteBuffer(), 0,
6. packetLen);
7. this.mysqlOutput.flush();
8. }
9. } catch (IOException ioEx) {
10. throw SQLError.createCommunicationsException(this.connection,
11. this.lastPacketSentTimeMs, this.lastPacketReceivedTimeMs, ioEx);
12. }
13. }
具体的获取数据库链接的序列图以下:
3.建立PreparedStatement,并设置参数
当建立完数据库链接以后,就能够经过conn.prepareStatement(sql) 来获取SQL执行环境PreparedStatement了,获取PreparedStatement的逻辑很是简单,会根据须要编译的SQL语句和Connection链接对象来建立一个JDBC4PreparedStatement对象,也就是相应SQL的执行环境了,具体代码以下:
Java代码
1. public java.sql.PreparedStatement prepareStatement(String sql,
2. int resultSetType, int resultSetConcurrency) throws SQLException {
3. checkClosed();
4. PreparedStatement pStmt = null;
6. //须要预编译的SQL语句
7. String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql): sql;
9. if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
10. canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
11. }
12. // 建立JDBC4PreapareStatement对象,这个SQL环境中持有预编译SQL语句及对应的数据库链接对象
13. pStmt = com.mysql.jdbc.PreparedStatement.getInstance(this, nativeSql,
14. this.database);
15. return pStmt;
16. }
当建立完SQL执行环境PreparedStatement对象以后,就能够设置一些自定义的参数了,最终会把参数值保存在JDBC4PreapareStatement的parameterValues字段,参数类型保存在parameterTypes中,以下代码:
Java代码
1. public void setInt(int parameterIndex, int x) throws SQLException {
2. int parameterIndexOffset = getParameterIndexOffset();
4. checkBounds(paramIndex, parameterIndexOffset);
5. byte[] parameterAsBytes = StringUtils.getBytes(String.value(x), this.charConverter,
6. this.charEncoding, this.connection
7. .getServerCharacterEncoding(), this.connection
8. .parserKnowsUnicode());
9. this.parameterStreams[paramIndex - 1 + parameterIndexOffset] = null;
10. //设置参数值
11. this.parameterValues[paramIndex - 1 + parameterIndexOffset] = parameterAsBytes;
12. //设置参数类型
13. this.parameterTypes[parameterIndex - 1 + getParameterIndexOffset()] = Types.INTEGER;
14. }
具体的建立PreparedStatement的序列图以下:
3.执行查询
建立完PreparedStatement以后,就一切准备就绪了,就能够经过 pstmt.executeQuery()来执行查询了。主要思路是根据SQL模板和设置的参数,解析成一条完整的SQL语句,最后根据MySQL协议,序列化成字节流,RPC发送给MySQL服务端。主要的处理过程以下:
Java代码
1. public java.sql.ResultSet executeQuery() throws SQLException {
2. checkClosed();
3. ConnectionImpl locallyScopedConn = this.connection;
4. CachedResultSetMetaData cachedMetadata = null;
5. synchronized (locallyScopedConn.getMutex()) {
6. if (doStreaming
7. && this.connection.getNetTimeoutForStreamingResults() > 0) {
8. locallyScopedConn.execSQL(this, "SET net_write_timeout="
9. + this.connection.getNetTimeoutForStreamingResults(),
10. -1, null, ResultSet.TYPE_FORWARD_ONLY,
11. ResultSet.CONCUR_READ_ONLY, false, this.currentCatalog,
12. null, false);
13. }
14. //解析封装须要发送的sql语句,序列化成MySQL协议对应的字节流
15. Buffer sendPacket = fillSendPacket();
17. if (locallyScopedConn.getCacheResultSetMetadata()) {
18. cachedMetadata = locallyScopedConn.getCachedMetaData(this.originalSql);
19. }
21. Field[] metadataFromCache = null;
23. // 执行sql语句,并获取MySQL发送过来的结果字节流,根据MySQL协议反序列化成ResultSet
24. this.results = executeInternal(-1, sendPacket,
25. doStreaming, true,
26. metadataFromCache, false);
27.
28. if (oldCatalog != null) {
29. locallyScopedConn.setCatalog(oldCatalog);
30. }
32. }
33. this.lastInsertId = this.results.getUpdateID();
34. return this.results;
35. }
接下来看下 fillSendPacket() 方法怎么来序列化成二进制字节流的,请看下面的代码分析
Java代码
1. protected Buffer fillSendPacket(byte[][] batchedParameterStrings,
2. InputStream[] batchedParameterStreams, boolean[] batchedIsStream,
3. int[] batchedStreamLengths) throws SQLException {
4. // 从connection的IO中获得发送数据包,首先清空其中的数据
5. Buffer sendPacket = this.connection.getIO().getSharedSendPacket();
6. sendPacket.clear();
8. //数据包的第一位为一个操做标识符(MysqlDefs.QUERY),表示驱动向服务器发送的链接的操做信号,包括有QUERY, PING, RELOAD, SHUTDOWN, PROCESS_INFO, QUIT, SLEEP等等,这个操做信号并非针 对sql语句操做而言的CRUD操做,从提供的几种参数来看,这个操做是针对服务器的一个操做。通常而言,使用到的都是MysqlDefs.QUERY,表示发送的是要执行sql语句的操做。
9. sendPacket.writeByte((byte) MysqlDefs.QUERY);
11. boolean useStreamLengths = this.connection
12. .getUseStreamLengthsInPrepStmts();
14. int ensurePacketSize = 0;
15. for (int i = 0; i < batchedParameterStrings.length; i++) {
16. if (batchedIsStream[i] && useStreamLengths) {
17. ensurePacketSize += batchedStreamLengths[i];
18. }
19. }
21. // 判断这个sendPacket的byte buffer够不够大,不够大的话,按照1.25倍来扩充buffer
22. if (ensurePacketSize != 0) {
23. sendPacket.ensureCapacity(ensurePacketSize);
24. }
26. //遍历全部的参数。在prepareStatement阶段的new ParseInfo()中,驱动曾经对sql语句进行过度割,若是含有以问号标识的参数占位符的话,就记录下这个占位符的位置,依据这个位置把sql分割成多段,放 在了一个名为staticSql的字符串中。这里就开始把sql语句进行拼装,把staticSql和parameterValues进行组合,放在操做符的后面
27. for (int i = 0; i < batchedParameterStrings.length; i++) {
28. if ((batchedParameterStrings[i] == null)
29. && (batchedParameterStreams[i] == null)) {
30. throw SQLError.createSQLException(Messages
31. .getString("PreparedStatement.40") //$NON-NLS-1$
32. + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);
33. }
35. //在sendPacket中加入staticSql数组中的元素,就是分割出来的没有”?”的sql语句,并把字符串转换成byte
36. sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);
38. if (batchedIsStream[i]) {
39. streamToBytes(sendPacket, batchedParameterStreams[i], true,
40. batchedStreamLengths[i], useStreamLengths);
41. } else {
43. //用batchedParameterStrings,也就是parameterValues来填充参数位置。在循环中,这个操做是跟在staticSql后面的,所以就把第i个参数加到了第i个staticSql段中。参考前面的staticSql的例 子,发现当循环结束的时候,原始sql语句最后一个”?”以前的sql语句就拼成了正确的语句了
44. sendPacket.writeBytesNoNull(batchedParameterStrings[i]);
45. }
46. }
48. // 因为在原始的包含问号的sql语句中,在最后一个”?”后面可能还有order by等语句,所以staticSql数组中的元素个数必定比参数的个数多1,因此这里把staticSqlString中最后一段sql语句放sendPacket中
49. sendPacket.writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);
50. return sendPacket;
51. }
准备好须要发送的MySQL协议的字节流后,就能够一路经过ConnectionImpl.execSQL--> MysqlIO.sqlQueryDirect --> MysqlIO.send -- >OutPutStram.write把字节流数据经过Socket发送给MySQL服务器,而后线程阻塞等待服务端返回结果数据,拿到数据后再根据MySQL协议反序列化成咱们熟悉的ResultSet对象。
具体执行SQL的序列图以下:
4、探究MyQL预编译
一.背景:
如今咱们淘宝持久化大多数是采用iBatis+MySQL作开发的,你们都知道,iBatis内置参数,形如#xxx#的,均采用了sql预编译的形式,举例以下:
Xml代码
1. <select id=”queryUserById” returnType=”userResult”>
2. SELECT * FROM user WHERE id =#id#
3. </select>
查看日志后,会发现这个sql执行时被记录以下,SELECT * FROM user WHERE id = ?
看过iBatis源码发现底层使用的就是JDBC的PreparedStatement,过程是先将带有占位符(即”?”)的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,而后直接执行带有真实参数的sql。查询了相关文档及资料后, 基本结论都是,使用预编译,能够提升sql的执行效率,而且有效地防止了sql注入。可是一直没有亲自去测试下,趁着最近看MySQL_JDBC的源码的契机,好好研究测试了下。测试结果出乎意料,发现原来一直以来我对PreparedStatement的理解是有误的。咱们平时使用的无论是JDBC仍是ORM框架iBatis默认都没有真正开启预编译,形如PreparedStatement( SELECT * FROMuser WHERE id = ? ),每次都是驱动拼好完整带参数的SQL( SELECT * FROM user WHERE id = 5 ),而后再发送给MySQL服务端,压根就没用到如PreparedStatement名字的功能。咨询了淘宝相关DBA 和相关TDDL同窗,确认了如今咱们线上使用的TDDL(JDBC)默认都是没有打开预编译的,可是通过测试确实预编译会快一点,DBA那边以后会详细测试并推广到线上。
接下来我会把探究过程跟你们分享并记录下。
二.问题:
个人疑问有两点:1.MySQL是否默认开启了预编译功能?若没有,将如何开启? 2.预编译是否能有效提高执行SQL的性能?
三.探究一:MySQL是否默认开启了预编译?
首先针对第一个问题。个人电脑上已经安装了MySQL,版本是5.1.9,打开配置文件my.ini,在"port=3306" 这一行下面加了配置:log=d:/logs/mysql_log.txt,这样就开启了MySQL日志功能,该日志主要记录MySQL执行sql的过程。重启MySQL,并创建一个库studb,在该库下建一个叫user的表,有id(主键)和username和password三个字段。
接着,我创建了一个简单的Java工程,引入JDBC驱动包mysql-connector-java-5.0.3-bin.jar。而后写了以下的代码:
Java代码
1. public static void main(String[] args) throws Exception{
2. String sql = "select * from userwhere id= ?";
3. Class.forName("com.mysql.jdbc.Driver");
4. Connection conn = null;
5. try{
6. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/studb?user=root&password=root");
7. PreparedStatement stmt = conn.prepareStatement(sql);
8. stmt.setString(1,5);
9. ResultSet rs = stmt.executeQuery();
10. rs.close();
11. stmt.close();
12. }catch(Exception e){
13. e.printStackTrace();
14. }finally{
15. conn.close();
16. }
17. }
执行这些代码后,打开刚才配置的mysql日志文件mysql_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 user where id = ?
1 Execute select * from user where id= 5
1 Close stmt
1 Quit
从MySQL日志能够清晰看到,server端执行了一次预编译Prepare及执行了一次Execute,预编译sql模板为“select * from user where id= ?”,说明MySQL5.1.19+ mysql-connector-java-5.0.3是默认开启预编译的。但仍是有不少疑惑,为何以前查阅资料,都说开启预编译是跟 useServerPrepStmts 参数有关的,因而将刚才代码里的JDBC链接修改以下:
Java代码
1. DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=false")
执行代码后,再次查看mysql日志:
1Query 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 user where id= 5
1Quit
果真,日志没有了prepare这一行,说明MySQL没有进行预编译。这意味着useServerPrepStmts这个参数是起效的,且默认值为true。
最后意识到useServerPrepStmts这个参数是JDBC的链接参数,这说明此问题与JDBC驱动程序可能有关系。打开MySQL官网,发如今线的官方文档很强大,支持全文检索,因而我将“useServerPrepStmts”作为关键字,搜索出了一些信息,原文以下:
Important change: Due to a number ofissues with the use of server-side prepared statements, Connector/J5.0.5 has disabled their use by default. The disabling of server-sideprepared statements does not affect the operation of the connector in any way.
To enable server-sideprepared statements, add the following configuration property to your connectorstring:
useServerPrepStmts=true
The default value of thisproperty is false (that is,Connector/J does not use server-side prepared statements)
这段文字说,Connector/J在5.0.5之后的版本,默认useServerPrepStmts参数为false,Connector/J就是咱们熟知的JDBC驱动程序。看来,若是咱们的驱动程序为5.0.5或以后的版本,想启用mysql预编译,就必须设置useServerPrepStmts=true。个人JDBC驱动用的是5.0.3,这个版本的useServerPrepStmts参数默认值是true。因而我将Java工程中的JDBC驱动程序替换为5.0.8的版本,去掉代码里JDBC链接中的useServerPrepStmts参数,再执行,发现mysql_log.txt的日志打印以下:
2 Query SHOW SESSIONVARIABLES
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 user where id= 5
2 Quit
果真,在mysql_log.txt日志里,prepare关键字没有了,说明 useServerPrepStmts 参数确实跟JDBC驱动版本有关。另外还查阅了相关MySQL的官方文档后,发现MySQL服务端是在4.1版本才开始支持预编译的,以后的版本都默认支持预编译。
第一个问题解决了,结论就是:要打开预编译功能跟MySQL版本及 MySQL Connector/J(JDBC驱动)版本都有关,首先MySQL服务端是在4.1版本以后才开始支持预编译的,以后的版本都默认支持预编译,而且预编译还与 MySQL Connector/J(JDBC驱动)的版本有关, Connector/J 5.0.5以前的版本默认支持预编译, Connector/J 5.0.5以后的版本默认不支持预编译, 因此咱们用的Connector/J 5.0.5驱动之后版本的话默认都是没有打开预编译的 (若是须要打开预编译,须要配置 useServerPrepStmts 参数)
四.探究二:预编译是否能有效提高执行SQL的性能?
首先,咱们要明白MySQL执行一个sql语句的过程。查了一些资料后,我得知,mysql执行脚本的大体过程以下:prepare(准备)-> optimize(优化)-> exec(物理执行),其中,prepare也就是咱们所说的编译。前面已经说过,对于同一个sql模板,若是能将prepare的结果缓存,之后若是再执行相同模板而参数不一样的sql,就能够节省掉prepare(准备)的环节,从而节省sql执行的成本。明白这一点后,我写了以下测试程序:
Java代码
1. public static void main(String []a) throws Exception{
2. String sql = "select * from user whereid = ?";
3. Class.forName("com.mysql.jdbc.Driver");
4. Connection conn = null;
5. try{
6. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/studb?user=root&password=root&useServerPrepStmts=true");
7. PreparedStatement stmt = conn.prepareStatement(sql);
8. stmt.setString(1,5);
9. ResultSet rs1 = stmt.executeQuery(); //第一次执行
10. s1.close();
11. stmt.setString(1,9);
12. ResultSet rs2 = stmt.executeQuery(); //第二次执行
13. rs2.close();
14. stmt.close();
15. }catch(Exception e){
16. e.printStackTrace();
17. }finally{
18. conn.close();
19. }
20. }
执行该程序后,查看mysql日志:
1Query 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 userwhere id = ?
1 Execute select * from user where id = 5
1 Execute select * from user where id = 9
1 Close stmt
1 Quit
按照日志看来,PreparedStatement从新设置sql参数后,并无从新prepare,看来预编译起到了效果。但刚才咱们使用的是同一个stmt,若是将stmt关闭,从新获取一个stmt呢?
Java代码
1. public static void main(String []a) throws Exception{
2. String sql = "select * from userwhere id = ?";
3. Class.forName("com.mysql.jdbc.Driver");
4. Connection conn = null;
5. try{
6. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/studb?user=root&password=root&useServerPrepStmts=true");
7. PreparedStatement stmt = conn.prepareStatement(sql);
8. stmt.setString(1,5);
9. ResultSet rs1 = stmt.executeQuery(); //第一次执行
10. rs1.close();
11. stmt.close();
12. stmt = conn.prepareStatement(sql); //从新获取一个statement
13. stmt.setString(1,9);
14. ResultSet rs2 = stmt.executeQuery(); //第二次执行
15. rs2.close();
16. stmt.close();
17. }catch(Exception e){
18. e.printStackTrace();
19. }finally{
20. conn.close();
21. }
22. }
mysql日志打印以下:
1Query 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 user where id=?
1 Execute select * from user where id= 5
1 Close stmt
1 Prepare select *from user where id = ?
1 Execute select * from user where id = 9
1 Close stmt
1 Quit
很明显,关闭stmt后再执行第二个sql,mysql就从新进行了一次预编译,这样是没法提升sql执行效率的。而在实际的应用场景中,咱们不可能保持同一个statement。那么,mysql如何缓存预编译结果呢?
搜索一些资料后得知,JDBC链接参数中有另一个重要的参数:cachePrepStmts ,设置为true后能够缓存预编译结果。因而我将测试代码中JDBC链接串改成了这样:
Java代码
1. conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/prepare_stmt_test?user=root&password=root&useServerPrepStmts=true&cachePrepStmts=true");
再执行代码后,发现mysql日志记录又变成了这样:
1 Prepare select * from user where id = ?
1 Execute select * from user where id = 5
1 Execute select * from user where id = 9
OK,如今咱们才正式开启了预编译,并开启了缓存预编译的功能。那么接下来咱们对预编译语句("select * from userwhere id = ?")进行性能测试,测试数据以下:
当不开启预编译功能时(String url ="jdbc:mysql://localhost:3306/studb"),作10次测试,100000个select总时间为(单位毫秒)
12321,12173,12159,12132,12604,12349,12621,12356,12899,12287
(每次查询一个RPC,每个查询,都会在mysql server端作一次编译及一次执行)
Mysql协议:xx xx xx xx QUERY .. .. .. .. .. ..
Mysql协议:xx xx xx xx QUERY .. .. .. .. .. ..
开启预编译,但不开启预编译缓存时(String url= "jdbc:mysql://localhost:3306/studb?useServerPrepStmts=true"),作10次测试,100000个select总时间为(单位毫秒)
21349,22860,27237,26848,27772,28100,23114,22897,20010,23211
(每次查询须要两个RPC,第一个RPC是编译,第二个RPC是执行,进测试数据能够看到这种其实与不打开预编译相比竟然还慢,由于多了一次RPC,网络开销在那里)
Mysql协议:xx xx xx xx PREPARE .. .. .. .. .. ..
Mysql协议:xx xx xx xx EXECUTE PS_ID ..
Mysql协议:xx xx xx xx PREPARE .. .. .. .. .. ..
Mysql协议:xx xx xx xx EXECUTE PS_ID ..
开启预编译,并开启预编译缓存时(String url ="jdbc:mysql://localhost:3306/studb?useServerPrepStmts=true&cachePrepStmts=true"),作10次测试,100000个select总时间为
8732,8655,8678,9693,8624,9874,8444,9660,8607,8780
(第一次两个RPC,以后都是一个RPC,第一次会由于编译sql模板走一次RPC,后面都只须要执行一次RPC,在mysql server端不须要编译,只须要执行)
Mysql协议:xx xx xx xx PREPARE .. .. .. .. .. ..
Mysql协议:xx xx xx xx EXECUTE PS_ID ..
Mysql协议:xx xx xx xx EXECUTE PS_ID ..
从测试结果看来,若开启预编译,但不开启预编译缓存,查询效率会有明显降低,由于须要走屡次RPC,且每一个查询都须要编译及执行;开启预编译而且打开预编译缓存的明显比不打开预编译的查询性能好30%左右(这个是本机测试,还须要更多验证)。
结论:对于Connector/J5.0.5之后的版本,若使用useServerPrepStmts=true开启预编译,则必定须要同时使用cachePrepStmts=true 开启预编译缓存,不然性能会降低,只有两者都开启,才算是真正开启了预编译功能,性能会比不开启预编译提高30%左右(这个多是我测试程序的缘由,有待进一步研究)
五.预编译JDBC驱动源码剖析
首先对于打开预编译的URL(String url ="jdbc:mysql://localhost:3306/studb?useServerPrepStmts=true&cachePrepStmts=true")获取数据库链接以后,本质是获取预编译语句pstmt = conn.prepareStatement(sql)时会向MySQL服务端发送一个RPC,发送一个预编译的SQL模板(驱动会拼接mysql预编译语句prepare s1 from 'select * fromuser where id = ?'),然会MySQL服务端会编译好收到的SQL模板,再会为此预编译模板语句分配一个serverStatementId发送给JDBC驱动,这样之后PreparedStatement就会持有当前预编译语句的服务端的serverStatementId,而且会把此 PreparedStatement缓存在当前数据库链接中,之后对于相同SQL模板的操做pstmt.executeUpdate(),都用相同的PreparedStatement,执行SQL时只须要发送serverStatementId和参数,节省一次SQL编译, 直接执行。而且对于每个链接(驱动端及Mysql服务端)都有本身的preparecache,具体的源码实现是在com.mysql.jdbc.ServerPreparedStatement中实现。