没想到你是这样的 JDBC

12-29 15:36

本文将介绍 MySQL Client 与 Server 的通信原理,以及 Java JDBC 的工作原理等。什么是JDBC 的 Type4,什么又是 Type 3? 更多精彩,请往下看。

一、 MySQL Client & Server

我们在进行数据库的操作时,总是通过 GUI 数据管理工具,或者命令行连接到 MySQL 的 Server 上,然后进行一系列数据库的创建、表与表内数据的操作等。

这个时候,这一系列 GUI管理工具,或者命令行,都是一个 MySQL 的 Client, 然后将 Client 的一系列操作命令,发送给 Server。 这里在发送时,Client 的命令都是根据 MySQL 规范,生成的一个个packet进行发送。

更直观的理解, MySQL 的 Client 和 Server 相当于是 Socket 通信中的一个 Client 与 Server, 彼此按照约定的协议格式进行通信。

二、 JDBC 是什么?

什么是 JDBC 呢? 你一定会脱口而出,不就是通过它连库嘛。 这么理解只是其中的一小部分,「洒洒水的啦」。

JDBC 全称:The Java Database Connectivity,要从两个方面来理解。

  1. API

  2. Driver

API , 首先是一个标准,并不针对特定的数据库,做为一个高层抽象,提供Java 语言与众多数据库之间的连通。 通过JDBC API,我们不再需要根据不同的数据库使用不同的操作方式,而是以一种标准的操作,实现『Write Once, Run anywhere』。

既然 API 是个标准,就需要有相对应的实现, 这里的 Driver 就是各个数据库厂商根据标准进行的针对实现。这也是为什么在应用开发时,连MySQL 使用 MySQL 的 connector,连接 Oracle 使用 Oracle 的驱动的原因。

毕竟如何和自己厂家的数据库交互,只有各个厂商自己清楚,所以根据标准,各个厂商开发自己的 Connector。

下图来自官方文档,来描述 JDBC 的作用以及请求中所处的位置。

图的左侧,也称为Type4, 是通过Driver 直接连接数据库 Server。这种也是最常用的,通过Driver ,将JDBC 的请求转成数据库服务器可以识别的协议格式。

图的右侧, 称为Type 3 是通过Driver,将JDBC 的请求转成 中间件的协议格式。

以MySQL为例,看到这里我们发现,其实 JDBC 的操作,本质上相当于是一个 MySQL 的 Client,通过 Driver,把应用里的查询、删除等操作「 翻译 」成了 MySQL Server 可识别的协议格式,再传递过去执行。

所以,整个JDBC 做的事情可以归结为以下三件:

  1. 创建数据库连接

  2. 发送 SQL statement

  3. 处理请求结果

JDBC 总结起来的两个部分,数据库服务提供方,开发XXXDriver,  应用开发者使用Driver 连接数据库,进行数据库操作。

这样应用开发者就不需要关心底层与数据库交互时的协议实现,如何进行请求连接,交互等,可以更专心到自己的业务。 否则,每个开发者都需要处理一次和数据交互的协议,繁琐而且不易,重复劳动。

三、MySQL connector-J 部分源码

有了上述的「理论」知识后,我们来看点干的。 MySQL 的驱动包是开源的,我们可以很方便的进行下载了解实现。

最传统的 JDBC 使用,一般都是通过以下这种方式:

  1. Connection c = DriverManager.getConnection(url, user,pwd);

  2. Statement stmt = c.createStatment

  3. stmt.executeQuery 拿结果

getConnection的时候一般都需要提供一个URL,这个URL也都是固定写法,比如mysql的是 jdbc:mysql://,这一部分是按照规范,同时在Driver的代码里,通过解析URL获取要连接到的主机,端口,以及其他的连接参数。

public Properties parseURL(String url, Properties defaults) throws java.sql.SQLException {

Properties urlProps = (defaults != null) ? new Properties(defaults) : new Properties();

if (url == null) {

return null;

}

if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)

&& !StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {

return null;

}

int beginningOfSlashes = url.indexOf("//");

if (StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)) {

urlProps.setProperty("socketFactory", "com.mysql.management.driverlaunched.ServerLauncherSocketFactory");

}

看这一部分源码可以发现,除了我们常用的url配置,还可以在其中进行loadbalance的配置等等。长了见识。:)

DriverManager.getConnection(xx,xx,xx) 这个方法最终会调用 Service Provider 已经加载的 Driver中可用的driver,调用driver的getConnection方法,对应到Mysql的源码,就是下方这个,重点是`com.mysql.jdbc.ConnectionImpl.getInstance`

public java.sql.Connection connect(String url, Properties info) {

if (url == null) {

throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.1"), SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null);

}

if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) {

return connectLoadBalanced(url, info);

} else if (StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {

return connectReplicationConnection(url, info);

}

Properties props = null;

if ((props = parseURL(url, info)) == null) {

return null;

}

if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) {

return connectFailover(url, info);

}

try {

Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);

return newConn;

}

再来看 getInstance具体做了啥?

protected static Connection getInstance(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url)

throws SQLException {

if (!Util.isJdbc4()) {

return new ConnectionImpl(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url);

}

return (Connection) Util.handleNewInstance(JDBC_4_CONNECTION_CTOR,

new Object[] { hostToConnectTo, Integer.valueOf(portToConnectTo), info, databaseToConnectTo, url }, null);

}

this.io = new MysqlIO (newHost, newPort, mergedProps, getSocketFactoryClassName(), getProxy(), getSocketTimeout(),

this.largeRowSizeThreshold.getValueAsInt());

this.io. doHandshake (this.user, this.password, this.database);

我们看,先通过MysqlIO创建了一个IO连接,然后进行握手

// save last exception to propagate to caller if connection fails

SocketException lastException = null;

// Need to loop through all possible addresses. Name lookup may return multiple addresses including IPv4 and IPv6 addresses. Some versions of

// MySQL don't listen on the IPv6 address so we try all addresses.

for (int i = 0; i < possibleAddresses.length; i++) {

try {

this.rawSocket = createSocket(props); // 这里创建了一个空的Socket对象

configureSocket(this.rawSocket, props); //将一些超时之类的属性设置到socket中

InetSocketAddress sockAddr = new InetSocketAddress(possibleAddresses[i], this.port); //获取host对应的ip地址等,再加上端口,组成一个Address

// bind to the local port if not using the ephemeral port

if (localSockAddr != null) {

this.rawSocket.bind(localSockAddr);

}

this.rawSocket.connect(sockAddr, getRealTimeout(connectTimeout)); //实际连接到服务器

连接Mysql的url中,可以分成好几类,例如可以连接到mysql进行loadbalanner, jdbc:mysql:loadbalancer//xxx 还有进行replicated

我们在使用JDBC连接时,一定会常使用PreparedStatement, 这个称为预编译sql,其中可以设置一些占位符

那这些占位符是啥时候填充进去的呢?

查看Mysql Connector 的源码,我们发现,实际前面的createPreparedStatment,setXX之类的时候,

只是设置到对应的变量里记录了下来,

在执行executeQuery的时候,会再从前面记录下来的变理中提取出来,做为值填充到原来的sql占位中去

整个sql做为一个packet发送过去。

这个时候也就更容易理解为啥预编译不容易被SQL 注入,而拼接SQL容易。 因为预编译在替换占位符时,即使你的值里有类似于 「--」 这一类的危险内容,或者 1==1, 都是做为一个column的value 来使用,而拼接SQL,则会放到完整的语句中,在执行时被全部解析,导致问题。

以下就是 MySQL Connector 在执行 sql 时的调用栈。

java.lang.Thread.State: RUNNABLE
  at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3633)
  at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2460)
  at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2625)
  at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2551)
  - locked <0x5a3> (a com.mysql.jdbc.JDBC4Connection)
  at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1861)
  at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1962)

整个背后其实原理也和我们前面说的一样,比较简单,是通过一个TCP Socket 方式,在获取到OutputStream,接装好的SQL,

在执行的时候,是写到这个Output里,发送到 Mysql的服务器。

返回值是怎么获取的呢? 是将返回的Buffer转换成ResultSet

ResultSetInternalMethods rs = readAllResults(callingStatement, maxRows, resultSetType, resultSetConcurrency, streamResults, catalog, resultPacket,

false, -1L, cachedMetadata);

此外,在实际的业务开发中,对于在代码中拿到的一个Connection,可能会遇到网络抖动,数据库服务异常等情况。有连接问题之前,我们可以先检测连接是否可用,来避免继续使用有问题的Connection,导致问题一直存在。

检测一个连接是否可用,可以通过执行一条最简单的 ` select 1 ` 来判断是否有异常,当然,在JDBC的标准里,也包含一个检查连接是否可用的方法 isValid

实现原理,对于MySQL 的Connctor-J客户端,是通过向Server发送一条ping的命令,来检测连接的状态。

总结一下,我们通过几个部分来介绍了 MySQL Client 与 Server 的交互原理,以及JDBC 是什么,是通过什么方式来和 Server 进行交互的。

顺道再分享下最近遇到的一个和数据库连接有关的小插曲。在处理一个问题,增加数据库连接检查之后,功能正确就上线了。上线不久,接到另一个服务提供方报警,说我们发送了其不能处理的数据库指令。 黑人问号脸。我只是通过获取数据库状态的一个getAttribute的方式来检查下连接啊。 据说他们收到的是show xxx status之类的指令。 那为啥不能识别呢?

仔细问了一下,是由于他们提供的特殊 Proxy 服务,只实现了MySQL 的部分指令解析,所以对应show xxx 不支持,而我们项目里默认以为全部的client 都支持全集指令,导致问题。之后改了一个检查方式解决了报警问题。

所以,在开发时,也需要再考虑下接入的服务,是否会按照规范,把全部内容实现了。

PS: 这篇内容在草稿箱里好久了,再不写就明年了 ^_^

相关阅读

Tomcat 中 的可插拔以及 SCI 的实现原理

Tomcat 是怎样处理 SpringBoot应用的?

数据源连接池的原理及Tomcat中的应用

☆★☆更多精彩内容 ☆★☆

一台机器上安装多个Tomcat 的原理(回复001)

监控Tomcat中的各种数据 (回复002)

启动Tomcat的安全机制(回复003)

乱码问题的原理及解决方式(回复007)

Tomcat 日志工作原理及配置(回复011)

web.xml 解析实现(回复 012)

线程池的原理( 回复 014)

Tomcat 的集群搭建原理与实现 (回复 015)

类加载器的原理 (回复 016)

类找不到等问题 (回复 017)

代码的热替换实现(回复 018)

Tomcat 进程自动退出问题 (回复 019)

为什么总是返回404? (回复 020)

...

PS: 对于一些 Tomcat常见问 题,在公众号的【 常见问题 】菜单中,有需要的朋友欢迎关注查看。

觉得本文对你有帮助?请分享给更多人 支持一下吧,谢谢

关注『  Tomcat那些事儿    ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

原文链接:http://mp.weixin.qq.com/s/2ogaBE18zY3-k0dWnQFDew?utm_source=tuicool&utm_medium=referral
标签: 数据库 JDBC
© 2014 TuiCode, Inc.