数据库中间件 MyCAT源码分析 —— PreparedStatement 重新入门
来源:互联网 发布:手机电视直播软件排行 编辑:程序博客网 时间:2024/06/06 04:36
title: MyCAT 源码分析 —— PreparedStatement 重新入门
date: 2017-07-17
tags:
categories: MyCAT
permalink: MyCAT/what-is-PreparedStatement
������关注微信公众号:【芋艿的后端小屋】有福利:
1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
4. 新的源码解析文章实时收到通知。每周更新一篇左右。
- 1. 概述
- 2. JDBC Client 实现
- 3. MyCAT Server 实现
- 3.1 创建 PreparedStatement
- 3.2 执行 SQL
- 4. 彩蛋
1. 概述
相信很多同学在学习 JDBC 时,都碰到 PreparedStatement
和 Statement
。究竟该使用哪个呢?最终很可能是懵里懵懂的看了各种总结,使用 PreparedStatement
。那么本文,通过 MyCAT 对 PreparedStatement
的实现对大家能够重新理解下。
本文主要分成两部分:
- JDBC Client 如何实现
PreparedStatement
。 - 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"); // PreparedStatement PreparedStatement ps = conn.prepareStatement("SELECT id, username, password FROM t_user WHERE id = ?"); ps.setLong(1, Math.abs(new Random().nextLong())); // execute ps.executeQuery(); }}
获取 MySQL 连接时,useServerPrepStmts=true
是非常非常非常重要的参数。如果不配置,PreparedStatement
实际是个假的 PreparedStatement
(新版本默认为 FALSE,据说部分老版本默认为 TRUE),未开启服务端级别的 SQL 预编译。
WHY ?来看下 JDBC 里面是怎么实现的。
// com.mysql.jdbc.ConnectionImpl.javapublic 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()) { // 从缓存中获取 pStmt synchronized (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 necessary if (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@Overridepublic 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.javapublic static void response(PreparedStatement pstmt, FrontendConnection c) { byte packetId = 0; // write preparedOk packet PreparedOkPacket 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 packet int parametersNumber = preparedOk.parametersNumber; if (parametersNumber > 0) { for (int i = 0; i < parametersNumber; i++) { FieldPacket field = 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 packet int columnsNumber = preparedOk.columnsNumber; if (columnsNumber > 0) { for (int i = 0; i < columnsNumber; i++) { FieldPacket field = 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 buffer c.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@Overridepublic 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); // 执行sql source.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 < len; 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.javapublic 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.javapublic 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》。
- 数据库中间件 MyCAT源码分析 —— PreparedStatement 重新入门
- 数据库中间件 MyCAT源码分析 —— 跨库两表Join
- 数据库中间件 MyCAT源码分析 —— XA分布式事务
- 数据库中间件 MyCAT 源码分析 —— SQL ON MongoDB
- 数据库中间件MyCAT源码分析:【单库单表】插入
- 数据库中间件MyCAT源码分析:【单库单表】查询
- 数据库中间件MyCat学习总结(1)——MyCat入门简介
- 数据库中间件-mycat快速入门
- 数据库中间件 MyCAT 源码解析 —— 分片结果合并(一)
- 数据库中间件Mycat源码解析(一):初识Mycat
- Mycat开发实践---数据库中间件架构分析
- 数据库中间件MyCat学习总结(2)——MyCat-Web原理介绍
- Mycat 数据库分库分表中间件
- Mycat 数据库分库分表中间件
- Mycat 数据库分库分表中间件
- Mycat 数据库分库分表中间件
- 数据库路由中间件MyCat
- 数据库路由中间件MyCat
- 在一个类的属性为一个类(例如一个学生类的成绩属性为成绩类,成绩属性是科目加分数)
- 线段 ——2D(如何判断线段是否相交)
- java异常处理
- UVA10305OrderingTasks
- Java 随机数、概率实现——Random类
- 数据库中间件 MyCAT源码分析 —— PreparedStatement 重新入门
- MathJax基础(13):极限符号
- [nginx]nginx高并发优化实践
- 朴素贝叶斯分类器
- 两个栈实现队列+两个队列实现栈+替换空格
- jsp,jquery学习笔记
- Unity 接入 Android
- Vue生命周期_Vue指令
- Java学习历程