数据库中间件 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. public class PreparedStatementDemo { 
  2.  
  3.     public static void main(String[] args) throws ClassNotFoundException, SQLException { 
  4.         // 1. 获得数据库连接 
  5.         Class.forName("com.mysql.jdbc.Driver"); 
  6.         Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:8066/dbtest?useServerPrepStmts=true""root""123456"); 
  7.  
  8.         // PreparedStatement 
  9.         PreparedStatement ps = conn.prepareStatement("SELECT id, username, password FROM t_user WHERE id = ?"); 
  10.         ps.setLong(1, Math.abs(new Random().nextLong())); 
  11.  
  12.         // execute 
  13.         ps.executeQuery(); 
  14.     } 
  15.  
  16.  

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

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

  1. // com.mysql.jdbc.ConnectionImpl.java 
  2. public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { 
  3.    synchronized (getConnectionMutex()) { 
  4.        checkClosed(); 
  5.  
  6.        PreparedStatement pStmt = null
  7.        boolean canServerPrepare = true
  8.        String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql) : sql; 
  9.  
  10.        if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) { 
  11.            canServerPrepare = canHandleAsServerPreparedStatement(nativeSql); 
  12.        } 
  13.  
  14.        if (this.useServerPreparedStmts && canServerPrepare) { 
  15.            if (this.getCachePreparedStatements()) { // 从缓存中获取 pStmt 
  16.                synchronized (this.serverSideStatementCache) { 
  17.                    pStmt = (com.mysql.jdbc.ServerPreparedStatement) this.serverSideStatementCache 
  18.                            .remove(makePreparedStatementCacheKey(this.database, sql)); 
  19.  
  20.                    if (pStmt != null) { 
  21.                        ((com.mysql.jdbc.ServerPreparedStatement) pStmt).setClosed(false); 
  22.                        pStmt.clearParameters(); // 清理上次留下的参数 
  23.                    } 
  24.  
  25.                    if (pStmt == null) { 
  26.                         // .... 省略代码 :向 Server 提交 SQL 预编译。 
  27.                    } 
  28.                } 
  29.            } else { 
  30.                try { 
  31.                    // 向 Server 提交 SQL 预编译。 
  32.                    pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency); 
  33.  
  34.                    pStmt.setResultSetType(resultSetType); 
  35.                    pStmt.setResultSetConcurrency(resultSetConcurrency); 
  36.                } catch (SQLException sqlEx) { 
  37.                    // Punt, if necessary 
  38.                    if (getEmulateUnsupportedPstmts()) { 
  39.                        pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false); 
  40.                    } else { 
  41.                        throw sqlEx; 
  42.                    } 
  43.                } 
  44.            } 
  45.        } else { 
  46.            pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false); 
  47.        } 
  48.  
  49.        return pStmt; 
  50.    } 
  51.  
  • 【前者】当 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,并返回 statementId 等信息。Client 发起 SQL 执行时,需要将 statementId 带给 MyCAT。核心代码如下:

  1. // ServerPrepareHandler.java 
  2. @Override 
  3. public void prepare(String sql) { 
  4. LOGGER.debug("use server prepare, sql: " + sql); 
  5.  
  6.    PreparedStatement pstmt = pstmtForSql.get(sql); 
  7.    if (pstmt == null) { // 缓存中获取 
  8.        // 解析获取字段个数和参数个数 
  9.        int columnCount = getColumnCount(sql); 
  10.        int paramCount = getParamCount(sql); 
  11.        pstmt = new PreparedStatement(++pstmtId, sql, columnCount, paramCount); 
  12.        pstmtForSql.put(pstmt.getStatement(), pstmt); 
  13.        pstmtForId.put(pstmt.getId(), pstmt); 
  14.    } 
  15.    PreparedStmtResponse.response(pstmt, source); 
  16. // PreparedStmtResponse.java 
  17. public static void response(PreparedStatement pstmt, FrontendConnection c) { 
  18.    byte packetId = 0; 
  19.  
  20.    // write preparedOk packet 
  21.    PreparedOkPacket preparedOk = new PreparedOkPacket(); 
  22.    preparedOk.packetId = ++packetId; 
  23.    preparedOk.statementId = pstmt.getId(); 
  24.    preparedOk.columnsNumber = pstmt.getColumnsNumber(); 
  25.    preparedOk.parametersNumber = pstmt.getParametersNumber(); 
  26.    ByteBuffer buffer = preparedOk.write(c.allocate(), c,true); 
  27.  
  28.    // write parameter field packet 
  29.    int parametersNumber = preparedOk.parametersNumber; 
  30.    if (parametersNumber > 0) { 
  31.        for (int i = 0; i < parametersNumber; i++) { 
  32.            FieldPacket field = new FieldPacket(); 
  33.            field.packetId = ++packetId; 
  34.            buffer = field.write(buffer, c,true); 
  35.        } 
  36.        EOFPacket eof = new EOFPacket(); 
  37.        eof.packetId = ++packetId; 
  38.        buffer = eof.write(buffer, c,true); 
  39.    } 
  40.  
  41.    // write column field packet 
  42.    int columnsNumber = preparedOk.columnsNumber; 
  43.    if (columnsNumber > 0) { 
  44.        for (int i = 0; i < columnsNumber; i++) { 
  45.            FieldPacket field = new FieldPacket(); 
  46.            field.packetId = ++packetId; 
  47.            buffer = field.write(buffer, c,true); 
  48.        } 
  49.        EOFPacket eof = new EOFPacket(); 
  50.        eof.packetId = ++packetId; 
  51.        buffer = eof.write(buffer, c,true); 
  52.    } 
  53.  
  54.    // send buffer 
  55.    c.write(buffer); 
  56.  

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

3.2 执行 SQL

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

 

 

 

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

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

核心代码如下:

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

4. 彩蛋

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

给老铁们额外加个🍗。

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

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

  1. // ServerPreparedStatement.java 
  2. public void close() throws SQLException { 
  3.    MySQLConnection locallyScopedConn = this.connection
  4.  
  5.    if (locallyScopedConn == null) { 
  6.        return; // already closed 
  7.    } 
  8.  
  9.    synchronized (locallyScopedConn.getConnectionMutex()) { 
  10.        if (this.isCached && isPoolable() && !this.isClosed) { 
  11.            clearParameters(); 
  12.            this.isClosed = true
  13.            this.connection.recachePreparedStatement(this); 
  14.            return
  15.        } 
  16.  
  17.        realClose(truetrue); 
  18.    } 
  19. // ConnectionImpl.java 
  20. public void recachePreparedStatement(ServerPreparedStatement pstmt) throws SQLException { 
  21.    synchronized (getConnectionMutex()) { 
  22.        if (getCachePreparedStatements() && pstmt.isPoolable()) { 
  23.            synchronized (this.serverSideStatementCache) { 
  24.                this.serverSideStatementCache.put(makePreparedStatementCacheKey(pstmt.currentCatalog, pstmt.originalSql), pstmt); 
  25.            } 
  26.        } 
  27.    } 
  28.  

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

责任编辑:庞桂玉 来源: 芋艿V的博客
相关推荐

2017-07-26 09:41:28

MyCATSQLMongoDB

2017-07-18 17:07:40

数据库 MyCATJoin

2017-11-27 05:06:42

数据库中间件cobar

2017-11-27 05:36:16

数据库中间件TDDL

2017-12-01 05:04:32

数据库中间件Atlas

2018-02-24 19:37:33

Java8数据库中间件

2009-01-20 10:45:55

Oracle数据库中间件

2017-05-23 18:55:05

mysql-proxy数据库架构

2011-08-10 13:03:58

CJDBC数据库集群

2020-04-10 17:00:33

Mycat分库分表SpringBoot

2017-11-30 08:56:14

数据库中间件架构师

2017-11-03 11:02:08

数据库中间件

2017-12-01 05:40:56

数据库中间件join

2017-12-11 13:30:49

Go语言数据库中间件

2017-11-27 06:01:37

数据库中间件中间层

2020-10-15 08:34:32

数据库中间件漫谈

2021-07-27 05:49:59

MySQL数据库中间件

2019-05-13 15:00:14

MySQLMyCat数据库

2018-11-07 15:30:19

数据库NewSQLNoSQL

2009-11-10 16:48:23

中间件操作系统数据库
点赞
收藏

51CTO技术栈公众号