热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

摘要:原创出处http:www.iocoder.cnMyCATwhat-is-PreparedStatement「芋道源码」欢迎转载,保留摘要,谢谢&#

摘要: 原创出处 http://www.iocoder.cn/MyCAT/what-is-PreparedStatement/ 「芋道源码」欢迎转载,保留摘要,谢谢!

本文主要基于 MyCAT 1.6.5 正式版

  • 1. 概述
  • 2. JDBC Client 实现
  • 3. MyCAT Server 实现
    • 3.1 创建 PreparedStatement
    • 3.2 执行 SQL
  • 4. 彩蛋


������关注微信公众号:【芋道源码】有福利:
1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
4. 新的源码解析文章实时收到通知。每周更新一篇左右


> 5. 认真的源码交流微信群。


1. 概述

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

本文主要分成两部分:

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

�� Let’s Go。

2. JDBC Client 实现

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

public class PreparedStatementDemo {public static void main(String[] args) throws ClassNotFoundException, SQLException {// 1. 获得数据库连接Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:8066/dbtest?useServerPrepStmts=true", "root", "123456");// PreparedStatementPreparedStatement ps = conn.prepareStatement("SELECT id, username, password FROM t_user WHERE id = ?");ps.setLong(1, Math.abs(new Random().nextLong()));// executeps.executeQuery();}}

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

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

// com.mysql.jdbc.ConnectionImpl.java
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {synchronized (getConnectionMutex()) {checkClosed();PreparedStatement pStmt = null;boolean canServerPrepare = true;String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql) : sql;if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);}if (this.useServerPreparedStmts && canServerPrepare) {if (this.getCachePreparedStatements()) { // 从缓存中获取 pStmtsynchronized (this.serverSideStatementCache) {pStmt = (com.mysql.jdbc.ServerPreparedStatement) this.serverSideStatementCache.remove(makePreparedStatementCacheKey(this.database, sql));if (pStmt != null) {((com.mysql.jdbc.ServerPreparedStatement) pStmt).setClosed(false);pStmt.clearParameters(); // 清理上次留下的参数}if (pStmt == null) {// .... 省略代码 :向 Server 提交 SQL 预编译。}}} else {try {// 向 Server 提交 SQL 预编译。pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);pStmt.setResultSetType(resultSetType);pStmt.setResultSetConcurrency(resultSetConcurrency);} catch (SQLException sqlEx) {// Punt, if necessaryif (getEmulateUnsupportedPstmts()) {pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);} else {throw sqlEx;}}}} else {pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);}return pStmt;}
}

  • 【前者】当 Client 开启 useServerPreparedStmts 并且 Server 支持 ServerPrepare,Client 会向 Server 提交 SQL 预编译请求

if (this.useServerPreparedStmts && canServerPrepare) {pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);
}

  • 【后者】当 Client 未开启 useServerPreparedStmts 或者 Server 不支持 ServerPrepare,Client 创建 PreparedStatement,不会向 Server 提交 SQL 预编译请求

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。核心代码如下:

// ServerPrepareHandler.java
@Override
public void prepare(String sql) {
LOGGER.debug("use server prepare, sql: " + sql);PreparedStatement pstmt = pstmtForSql.get(sql);if (pstmt == null) { // 缓存中获取// 解析获取字段个数和参数个数int columnCount = getColumnCount(sql);int paramCount = getParamCount(sql);pstmt = new PreparedStatement(++pstmtId, sql, columnCount, paramCount);pstmtForSql.put(pstmt.getStatement(), pstmt);pstmtForId.put(pstmt.getId(), pstmt);}PreparedStmtResponse.response(pstmt, source);
}
// PreparedStmtResponse.java
public static void response(PreparedStatement pstmt, FrontendConnection c) {byte packetId = 0;// write preparedOk packetPreparedOkPacket preparedOk = new PreparedOkPacket();preparedOk.packetId = ++packetId;preparedOk.statementId = pstmt.getId();preparedOk.columnsNumber = pstmt.getColumnsNumber();preparedOk.parametersNumber = pstmt.getParametersNumber();ByteBuffer buffer = preparedOk.write(c.allocate(), c,true);// write parameter field packetint parametersNumber = preparedOk.parametersNumber;if (parametersNumber > 0) {for (int i = 0; i new FieldPacket();field.packetId = ++packetId;buffer = field.write(buffer, c,true);}EOFPacket eof = new EOFPacket();eof.packetId = ++packetId;buffer = eof.write(buffer, c,true);}// write column field packetint columnsNumber = preparedOk.columnsNumber;if (columnsNumber > 0) {for (int i = 0; i new FieldPacket();field.packetId = ++packetId;buffer = field.write(buffer, c,true);}EOFPacket eof = new EOFPacket();eof.packetId = ++packetId;buffer = eof.write(buffer, c,true);}// send bufferc.write(buffer);
}

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

3.2 执行 SQL

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

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

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

核心代码如下:

// ServerPrepareHandler.java
@Override
public void execute(byte[] data) {long pstmtId = ByteUtil.readUB4(data, 5);PreparedStatement pstmt = null;if ((pstmt = pstmtForId.get(pstmtId)) == null) {source.writeErrMessage(ErrorCode.ER_ERROR_WHEN_EXECUTING_COMMAND, "Unknown pstmtId when executing.");} else {// 参数读取ExecutePacket packet = new ExecutePacket(pstmt);try {packet.read(data, source.getCharset());} catch (UnsupportedEncodingException e) {source.writeErrMessage(ErrorCode.ER_ERROR_WHEN_EXECUTING_COMMAND, e.getMessage());return;}BindValue[] bindValues = packet.values;// 还原sql中的动态参数为实际参数值String sql = prepareStmtBindValue(pstmt, bindValues);// 执行sqlsource.getSession2().setPrepared(true);source.query(sql);}
}private String prepareStmtBindValue(PreparedStatement pstmt, BindValue[] bindValues) {String sql = pstmt.getStatement();int[] paramTypes = pstmt.getParametersType();StringBuilder sb = new StringBuilder();int idx = 0;for (int i = 0, len = sql.length(); i char c = sql.charAt(i);if (c != '?') {sb.append(c);continue;}// 处理占位符?int paramType = paramTypes[idx];BindValue bindValue = bindValues[idx];idx++;// 处理字段为空的情况if (bindValue.isNull) {sb.append("NULL");continue;}// 非空情况, 根据字段类型获取值switch (paramType & 0xff) {case Fields.FIELD_TYPE_TINY:sb.append(String.valueOf(bindValue.byteBinding));break;case Fields.FIELD_TYPE_SHORT:sb.append(String.valueOf(bindValue.shortBinding));break;case Fields.FIELD_TYPE_LONG:sb.append(String.valueOf(bindValue.intBinding));break;// .... 省略非核心代码}}return sb.toString();
}

4. 彩蛋

知识星球

�� 看到此处是不是真爱?!反正我信了。
给老铁们额外加个��。

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

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

// ServerPreparedStatement.java
public void close() throws SQLException {MySQLConnection locallyScopedConn = this.connection;if (locallyScopedConn == null) {return; // already closed}synchronized (locallyScopedConn.getConnectionMutex()) {if (this.isCached && isPoolable() && !this.isClosed) {clearParameters();this.isClosed = true;this.connection.recachePreparedStatement(this);return;}realClose(true, true);}
}
// ConnectionImpl.java
public void recachePreparedStatement(ServerPreparedStatement pstmt) throws SQLException {synchronized (getConnectionMutex()) {if (getCachePreparedStatements() && pstmt.isPoolable()) {synchronized (this.serverSideStatementCache) {this.serverSideStatementCache.put(makePreparedStatementCacheKey(pstmt.currentCatalog, pstmt.originalSql), pstmt);}}}
}

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

�� 这个“彩蛋”还满意么?请关注我的公众号:芋道源码。下一篇更新:《MyCAT源码解析 —— MongoDB》,极大可能就在本周噢。

另外推荐一篇文章:《JDBC PreparedStatement》。


������关注微信公众号:【芋道源码】有福利:
1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
4. 新的源码解析文章实时收到通知。每周更新一篇左右
5. 认真的源码交流微信群。


推荐阅读
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • Java SE从入门到放弃(三)的逻辑运算符详解
    本文详细介绍了Java SE中的逻辑运算符,包括逻辑运算符的操作和运算结果,以及与运算符的不同之处。通过代码演示,展示了逻辑运算符的使用方法和注意事项。文章以Java SE从入门到放弃(三)为背景,对逻辑运算符进行了深入的解析。 ... [详细]
  • 五、RabbitMQ Java Client基本使用详解
    JavaClient的5.x版本系列需要JDK8,用于编译和运行。在Android上,仅支持Android7.0或更高版本。4.x版本系列支持7.0之前 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 3.223.28周学习总结中的贪心作业收获及困惑
    本文是对3.223.28周学习总结中的贪心作业进行总结,作者在解题过程中参考了他人的代码,但前提是要先理解题目并有解题思路。作者分享了自己在贪心作业中的收获,同时提到了一道让他困惑的题目,即input details部分引发的疑惑。 ... [详细]
author-avatar
伴生约定_879
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有