数据库中间件 MyCAT源码分析 —— PreparedStatement 重新入门

相信很多同学在学习 JDBC 时,都碰到 PreparedStatement 和 Statement。究竟该使用哪个呢?最终很可能是懵里懵懂的看了各种总结,使用 PreparedStatement。那么本文,通过 MyCAT 对 PreparedStatement 的实现对大家能够重新理解下。

相信很多同学在学习 JDBC 时,都碰到 PreparedStatement 和 Statement。究竟该使用哪个呢?最终很可能是懵里懵懂的看了各种总结,使用 PreparedStatement。那么本文,通过 MyCAT 对 PreparedStatement 的实现对大家能够重新理解下。

1. 概述

相信很多同学在学习 JDBC 时,都碰到 PreparedStatement 和 Statement。究竟该使用哪个呢?最终很可能是懵里懵懂的看了各种总结,使用 PreparedStatement。那么本文,通过 MyCAT 对 PreparedStatement 的实现对大家能够重新理解下。

本文主要分成两部分:

  1. JDBC Client 如何实现 PreparedStatement。
  2. MyCAT Server 如何处理 PreparedStatement。

😈 Let's Go。

2. JDBC Client 实现

首先,我们来看一段大家最喜欢复制粘贴之一的代码,JDBC PreparedStatement 查询 MySQL 数据库

  1. publicclassPreparedStatementDemo{
  2. publicstaticvoidmain(String[]args)throwsClassNotFoundException,SQLException{
  3. //1.获得数据库连接
  4. Class.forName("com.mysql.jdbc.Driver");
  5. Connectionconn=DriverManager.getConnection("jdbc:mysql://127.0.0.1:8066/dbtest?useServerPrepStmts=true","root","123456");
  6. //PreparedStatement
  7. PreparedStatementps=conn.prepareStatement("SELECTid,username,passwordFROMt_userWHEREid=?");
  8. ps.setLong(1,Math.abs(newRandom().nextLong()));
  9. //execute
  10. ps.executeQuery();
  11. }
  12. }

获取 MySQL 连接时,useServerPrepStmts=true 是非常非常非常重要的参数。如果不配置,PreparedStatement 实际是个假的 PreparedStatement(新版本默认为 FALSE,据说部分老版本默认为 TRUE),未开启服务端级别的 SQL 预编译。

WHY ?来看下 JDBC 里面是怎么实现的。

  1. //com.mysql.jdbc.ConnectionImpl.java
  2. publicPreparedStatementprepareStatement(Stringsql,intresultSetType,intresultSetConcurrency)throwsSQLException{
  3. synchronized(getConnectionMutex()){
  4. checkClosed();
  5. PreparedStatementpStmt=null;
  6. booleancanServerPrepare=true;
  7. StringnativeSql=getProcessEscapeCodesForPrepStmts()?nativeSQL(sql):sql;
  8. if(this.useServerPreparedStmts&&getEmulateUnsupportedPstmts()){
  9. canServerPrepare=canHandleAsServerPreparedStatement(nativeSql);
  10. }
  11. if(this.useServerPreparedStmts&&canServerPrepare){
  12. if(this.getCachePreparedStatements()){//从缓存中获取pStmt
  13. synchronized(this.serverSideStatementCache){
  14. pStmt=(com.mysql.jdbc.ServerPreparedStatement)this.serverSideStatementCache
  15. .remove(makePreparedStatementCacheKey(this.database,sql));
  16. if(pStmt!=null){
  17. ((com.mysql.jdbc.ServerPreparedStatement)pStmt).setClosed(false);
  18. pStmt.clearParameters();//清理上次留下的参数
  19. }
  20. if(pStmt==null){
  21. //....省略代码:向Server提交SQL预编译。
  22. }
  23. }
  24. }else{
  25. try{
  26. //向Server提交SQL预编译。
  27. pStmt=ServerPreparedStatement.getInstance(getMultiHostSafeProxy(),nativeSql,this.database,resultSetType,resultSetConcurrency);
  28. pStmt.setResultSetType(resultSetType);
  29. pStmt.setResultSetConcurrency(resultSetConcurrency);
  30. }catch(SQLExceptionsqlEx){
  31. //Punt,ifnecessary
  32. if(getEmulateUnsupportedPstmts()){
  33. pStmt=(PreparedStatement)clientPrepareStatement(nativeSql,resultSetType,resultSetConcurrency,false);
  34. }else{
  35. throwsqlEx;
  36. }
  37. }
  38. }
  39. }else{
  40. pStmt=(PreparedStatement)clientPrepareStatement(nativeSql,resultSetType,resultSetConcurrency,false);
  41. }
  42. returnpStmt;
  43. }
  44. }
  • 【前者】当 Client 开启 useServerPreparedStmts 并且 Server 支持 ServerPrepare,Client 会向 Server 提交 SQL 预编译请求。
  1. if(this.useServerPreparedStmts&&canServerPrepare){
  2. pStmt=ServerPreparedStatement.getInstance(getMultiHostSafeProxy(),nativeSql,this.database,resultSetType,resultSetConcurrency);
  3. }
  • 【后者】当 Client 未开启 useServerPreparedStmts 或者 Server 不支持 ServerPrepare,Client 创建 PreparedStatement,不会向 Server 提交 SQL 预编译请求。
  1. pStmt=(PreparedStatement)clientPrepareStatement(nativeSql,resultSetType,resultSetConcurrency,false);

即使这样,究竟为什么性能会更好呢?

  • 【前者】返回的 PreparedStatement 对象类是 JDBC42ServerPreparedStatement.java,后续每次执行 SQL 只需将对应占位符?对应的值提交给 Server即可,减少网络传输和 SQL 解析开销。
  • 【后者】返回的 PreparedStatement 对象类是 JDBC42PreparedStatement.java,后续每次执行 SQL 需要将完整的 SQL 提交给 Server,增加了网络传输和 SQL 解析开销。

🌚:【前者】性能一定比【后者】好吗?相信你已经有了正确的答案。

3. MyCAT Server 实现

3.1 创建 PreparedStatement

该操作对应 Client conn.prepareStatement(....)。

数据库中间件 MyCAT源码分析 —— PreparedStatement 重新入门

MyCAT 接收到请求后,创建 PreparedStatement,并返回 statementId 等信息。Client 发起 SQL 执行时,需要将 statementId 带给 MyCAT。核心代码如下:

  1. //ServerPrepareHandler.java
  2. @Override
  3. publicvoidprepare(Stringsql){
  4. LOGGER.debug("useserverprepare,sql:"+sql);
  5. PreparedStatementpstmt=pstmtForSql.get(sql);
  6. if(pstmt==null){//缓存中获取
  7. //解析获取字段个数和参数个数
  8. intcolumnCount=getColumnCount(sql);
  9. intparamCount=getParamCount(sql);
  10. pstmt=newPreparedStatement(++pstmtId,sql,columnCount,paramCount);
  11. pstmtForSql.put(pstmt.getStatement(),pstmt);
  12. pstmtForId.put(pstmt.getId(),pstmt);
  13. }
  14. PreparedStmtResponse.response(pstmt,source);
  15. }
  16. //PreparedStmtResponse.java
  17. publicstaticvoidresponse(PreparedStatementpstmt,FrontendConnectionc){
  18. bytepacketId=0;
  19. //writepreparedOkpacket
  20. PreparedOkPacketpreparedOk=newPreparedOkPacket();
  21. preparedOk.packetId=++packetId;
  22. preparedOk.statementId=pstmt.getId();
  23. preparedOk.columnsNumber=pstmt.getColumnsNumber();
  24. preparedOk.parametersNumber=pstmt.getParametersNumber();
  25. ByteBufferbuffer=preparedOk.write(c.allocate(),c,true);
  26. //writeparameterfieldpacket
  27. intparametersNumber=preparedOk.parametersNumber;
  28. if(parametersNumber>0){
  29. for(inti=0;i<parametersNumber;i++){
  30. FieldPacketfield=newFieldPacket();
  31. field.packetId=++packetId;
  32. buffer=field.write(buffer,c,true);
  33. }
  34. EOFPacketeof=newEOFPacket();
  35. eof.packetId=++packetId;
  36. buffer=eof.write(buffer,c,true);
  37. }
  38. //writecolumnfieldpacket
  39. intcolumnsNumber=preparedOk.columnsNumber;
  40. if(columnsNumber>0){
  41. for(inti=0;i<columnsNumber;i++){
  42. FieldPacketfield=newFieldPacket();
  43. field.packetId=++packetId;
  44. buffer=field.write(buffer,c,true);
  45. }
  46. EOFPacketeof=newEOFPacket();
  47. eof.packetId=++packetId;
  48. buffer=eof.write(buffer,c,true);
  49. }
  50. //sendbuffer
  51. c.write(buffer);
  52. }

每个连接之间,PreparedStatement 不共享,即不同连接,即使 SQL相同,对应的 PreparedStatement 不同。

3.2 执行 SQL

该操作对应 Client conn.execute(....)。

数据库中间件 MyCAT源码分析 —— PreparedStatement 重新入门

MyCAT 接收到请求后,将 PreparedStatement 使用请求的参数格式化成可执行的 SQL 进行执行。伪代码如下:

  1. Stringsql=pstmt.sql.format(request.params);
  2. execute(sql);

核心代码如下:

  1. //ServerPrepareHandler.java
  2. @Override
  3. publicvoidexecute(byte[]data){
  4. longpstmtId=ByteUtil.readUB4(data,5);
  5. PreparedStatementpstmt=null;
  6. if((pstmt=pstmtForId.get(pstmtId))==null){
  7. source.writeErrMessage(ErrorCode.ER_ERROR_WHEN_EXECUTING_COMMAND,"UnknownpstmtIdwhenexecuting.");
  8. }else{
  9. //参数读取
  10. ExecutePacketpacket=newExecutePacket(pstmt);
  11. try{
  12. packet.read(data,source.getCharset());
  13. }catch(UnsupportedEncodingExceptione){
  14. source.writeErrMessage(ErrorCode.ER_ERROR_WHEN_EXECUTING_COMMAND,e.getMessage());
  15. return;
  16. }
  17. BindValue[]bindValues=packet.values;
  18. //还原sql中的动态参数为实际参数值
  19. Stringsql=prepareStmtBindValue(pstmt,bindValues);
  20. //执行sql
  21. source.getSession2().setPrepared(true);
  22. source.query(sql);
  23. }
  24. }
  25. privateStringprepareStmtBindValue(PreparedStatementpstmt,BindValue[]bindValues){
  26. Stringsql=pstmt.getStatement();
  27. int[]paramTypes=pstmt.getParametersType();
  28. StringBuildersb=newStringBuilder();
  29. intidx=0;
  30. for(inti=0,len=sql.length();i<len;i++){
  31. charc=sql.charAt(i);
  32. if(c!='?'){
  33. sb.append(c);
  34. continue;
  35. }
  36. //处理占位符?
  37. intparamType=paramTypes[idx];
  38. BindValuebindValue=bindValues[idx];
  39. idx++;
  40. //处理字段为空的情况
  41. if(bindValue.isNull){
  42. sb.append("NULL");
  43. continue;
  44. }
  45. //非空情况,根据字段类型获取值
  46. switch(paramType&0xff){
  47. caseFields.FIELD_TYPE_TINY:
  48. sb.append(String.valueOf(bindValue.byteBinding));
  49. break;
  50. caseFields.FIELD_TYPE_SHORT:
  51. sb.append(String.valueOf(bindValue.shortBinding));
  52. break;
  53. caseFields.FIELD_TYPE_LONG:
  54. sb.append(String.valueOf(bindValue.intBinding));
  55. break;
  56. //....省略非核心代码
  57. }
  58. }
  59. returnsb.toString();
  60. }

4. 彩蛋

💯 看到此处是不是真爱?!反正我信了。

给老铁们额外加个🍗。

细心的同学们可能已经注意到 JDBC Client 是支持缓存 PreparedStatement,无需每次都让 Server 进行创建。

当配置 MySQL 数据连接 cachePrepStmts=true 时开启 Client 级别的缓存。But,此处的缓存又和一般的缓存不一样,是使用 remove 的方式获得的,并且创建好 PreparedStatement 时也不添加到缓存。那什么时候添加缓存呢?在 pstmt.close() 时,并且pstmt 是通过缓存获取时,添加到缓存。核心代码如下:

  1. //ServerPreparedStatement.java
  2. publicvoidclose()throwsSQLException{
  3. MySQLConnectionlocallyScopedConn=this.connection;
  4. if(locallyScopedConn==null){
  5. return;//alreadyclosed
  6. }
  7. synchronized(locallyScopedConn.getConnectionMutex()){
  8. if(this.isCached&&isPoolable()&&!this.isClosed){
  9. clearParameters();
  10. this.isClosed=true;
  11. this.connection.recachePreparedStatement(this);
  12. return;
  13. }
  14. realClose(true,true);
  15. }
  16. }
  17. //ConnectionImpl.java
  18. publicvoidrecachePreparedStatement(ServerPreparedStatementpstmt)throwsSQLException{
  19. synchronized(getConnectionMutex()){
  20. if(getCachePreparedStatements()&&pstmt.isPoolable()){
  21. synchronized(this.serverSideStatementCache){
  22. this.serverSideStatementCache.put(makePreparedStatementCacheKey(pstmt.currentCatalog,pstmt.originalSql),pstmt);
  23. }
  24. }
  25. }
  26. }

为什么要这么实现?PreparedStatement 是有状态的变量,我们会去 setXXX(pos, value),一旦多线程共享,会导致错乱。

©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经

(0)
打赏 微信扫码打赏 微信扫码打赏 支付宝扫码打赏 支付宝扫码打赏
清一色的头像清一色管理团队
上一篇 2023年5月6日 09:40
下一篇 2023年5月6日 09:40

相关推荐

发表评论

登录后才能评论

联系我们

在线咨询:1643011589-QQbutton

手机:13798586780

QQ/微信:1074760229

QQ群:551893940

工作时间:工作日9:00-18:00,节假日休息

关注微信