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

本文涉及的产品
云原生网关 MSE Higress,422元/月
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 数据库中间件 MyCAT源码分析 —— PreparedStatement 重新入门

1. 概述

2. JDBC Client 实现

3. MyCAT Server 实现

3.1 创建 PreparedStatement

3.2 执行 SQL

4. 彩蛋


1. 概述

相信很多同学在学习 JDBC 时,都碰到 PreparedStatementStatement。究竟该使用哪个呢?最终很可能是懵里懵懂的看了各种总结,使用 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.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
())
{
// 从缓存中获取 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 支持 ServerPrepareClient 会向 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(....)

image.png

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 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(....)

image.png

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
);
// 执行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.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),一旦多线程共享,会导致错乱。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2月前
|
存储 SQL 分布式数据库
OceanBase 入门:分布式数据库的基础概念
【8月更文第31天】在当今的大数据时代,随着业务规模的不断扩大,传统的单机数据库已经难以满足高并发、大数据量的应用需求。分布式数据库应运而生,成为解决这一问题的有效方案之一。本文将介绍一款由阿里巴巴集团自主研发的分布式数据库——OceanBase,并通过一些基础概念和实际代码示例来帮助读者理解其工作原理。
212 0
|
11天前
|
SQL Java 数据库连接
如何使用`DriverManager.getConnection()`连接数据库,并利用`PreparedStatement`执行参数化查询,有效防止SQL注入。
【10月更文挑战第6天】在代码与逻辑交织的世界中,我从一名数据库新手出发,通过不断探索与实践,最终成为熟练掌握JDBC的开发者。这段旅程充满挑战与惊喜,从建立数据库连接到执行SQL语句,再到理解事务管理和批处理等高级功能,每一步都让我对JDBC有了更深的认识。示例代码展示了如何使用`DriverManager.getConnection()`连接数据库,并利用`PreparedStatement`执行参数化查询,有效防止SQL注入。
47 5
|
3月前
|
SQL NoSQL Oracle
IT入门知识第四部分《数据库》(4/10)(二)
IT入门知识第四部分《数据库》(4/10)(二)
38 0
|
3月前
|
存储 SQL 关系型数据库
IT入门知识第四部分《数据库》(4/10)(一)
IT入门知识第四部分《数据库》(4/10)(一)
46 0
|
14天前
|
SQL 存储 数据管理
SQL数据库的使用指南:从入门到精通
随着信息技术的飞速发展,数据库已成为各类企业和组织不可或缺的一部分。作为最流行的数据库管理系统之一,SQL数据库广泛应用于各种场景,如数据存储、数据管理、数据分析等。本文将详细介绍SQL数据库的使用方法,帮助初学者快速入门,并帮助有经验的开发者深化理解。一、SQL数据库基础首先,我们需要理解SQL数
64 2
|
14天前
|
SQL 存储 数据库
初识SQL数据库教程——从入门到精通
随着信息技术的飞速发展,数据库技术已成为计算机领域的重要组成部分。作为最流行的数据库管理系统之一,SQL数据库广泛应用于各类企业和组织的数据管理中。本文将带领读者从入门到精通,学习SQL数据库的相关知识。一、SQL数据库概述SQL(StructuredQueryLanguage)是一种用于管理关系数
31 2
|
2月前
|
SQL 存储 NoSQL
Redis6入门到实战------ 一、NoSQL数据库简介
这篇文章是关于NoSQL数据库的简介,讨论了技术发展、NoSQL数据库的概念、适用场景、不适用场景,以及常见的非关系型数据库。文章还提到了Web1.0到Web2.0时代的技术演进,以及解决CPU、内存和IO压力的方法,并对比了行式存储和列式存储数据库的特点。
Redis6入门到实战------ 一、NoSQL数据库简介
|
2月前
|
SQL 数据库
Spring5入门到实战------13、使用JdbcTemplate操作数据库(批量增删改)。具体代码+讲解 【下篇】
这篇文章是Spring5框架的实战教程,深入讲解了如何使用JdbcTemplate进行数据库的批量操作,包括批量添加、批量修改和批量删除的具体代码实现和测试过程,并通过完整的项目案例展示了如何在实际开发中应用这些技术。
Spring5入门到实战------13、使用JdbcTemplate操作数据库(批量增删改)。具体代码+讲解 【下篇】
|
1月前
|
SQL Oracle 关系型数据库
Oracle数据库管理:从入门到精通
表(Table):表是Oracle数据库中存储数据的基本结构单元,由行(Row)和列(Column)组成。每行代表一个记录,每列代表一个字段。 SQL(Structured Query Language):SQL是Oracle数据库的核心语言,用于与数据库交互,执行查询、插入、更新和删除等操作。 数据库实例与数据库:在Oracle中,数据库实例是一组后台进程和内存结构,用于管理数据库并提供服务。而数据库则是数据的物理存储,包括数据文件、控制文件、联机日志和参数文件等。
60 0
|
1月前
|
SQL Java OLAP
Hologres 入门:实时分析数据库的新选择
【9月更文第1天】在大数据和实时计算领域,数据仓库和分析型数据库的需求日益增长。随着业务对数据实时性要求的提高,传统的批处理架构已经难以满足现代应用的需求。阿里云推出的 Hologres 就是为了解决这个问题而生的一款实时分析数据库。本文将带你深入了解 Hologres 的基本概念、优势,并通过示例代码展示如何使用 Hologres 进行数据处理。
231 2