热门标签 | 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. 认真的源码交流微信群。


推荐阅读
  • 本文介绍了如何在MySQL中将零值替换为先前的非零值的方法,包括使用内联查询和更新查询。同时还提供了选择正确值的方法。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • ubuntu用sqoop将数据从hive导入mysql时,命令: ... [详细]
  • 在数据分析工作中,我们通常会遇到这样的问题,一个业务部门由若干业务组构成,需要筛选出每个业务组里业绩前N名的业务员。这其实是一个分组排序的 ... [详细]
  • Oracle Database 10g许可授予信息及高级功能详解
    本文介绍了Oracle Database 10g许可授予信息及其中的高级功能,包括数据库优化数据包、SQL访问指导、SQL优化指导、SQL优化集和重组对象。同时提供了详细说明,指导用户在Oracle Database 10g中如何使用这些功能。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文详细介绍了MysqlDump和mysqldump进行全库备份的相关知识,包括备份命令的使用方法、my.cnf配置文件的设置、binlog日志的位置指定、增量恢复的方式以及适用于innodb引擎和myisam引擎的备份方法。对于需要进行数据库备份的用户来说,本文提供了一些有价值的参考内容。 ... [详细]
  • 安卓select模态框样式改变_微软Office风格的多端(Web、安卓、iOS)组件库——Fabric UI...
    介绍FabricUI是微软开源的一套Office风格的多端组件库,共有三套针对性的组件,分别适用于web、android以及iOS,Fab ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 本文介绍了在Mac上搭建php环境后无法使用localhost连接mysql的问题,并通过将localhost替换为127.0.0.1或本机IP解决了该问题。文章解释了localhost和127.0.0.1的区别,指出了使用socket方式连接导致连接失败的原因。此外,还提供了相关链接供读者深入了解。 ... [详细]
  • 本文详细介绍了MySQL表分区的创建、增加和删除方法,包括查看分区数据量和全库数据量的方法。欢迎大家阅读并给予点评。 ... [详细]
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社区 版权所有