???關注微信公眾號:【芋艿的後端小屋】有福利:
- RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
- RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
- 您對於原始碼的疑問每條留言都將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢。
- 新的原始碼解析文章實時收到通知。每週更新一篇左右。
- 認真的原始碼交流微信群。
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.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 支援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 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
@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)
,一旦多執行緒共享,會導致錯亂。
? 這個“彩蛋”還滿意麼?請關注我的公眾號:芋艿的後端小屋。下一篇更新:《MyCAT原始碼解析 —— MongoDB》,極大可能就在本週噢。
另外推薦一篇文章:《JDBC PreparedStatement》。