没想到你是这样的JDBC

开发 开发工具
本文将介绍 MySQL Client 与 Server 的通信原理,以及 Java JDBC 的工作原理等。什么是JDBC 的 Type4,什么又是 Type 3?

本文将介绍 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,要从两个方面来理解。

  • API
  • Driver

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

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

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

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

 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 使用,一般都是通过以下这种方式:

  • Connection c = DriverManager.getConnection(url, user,pwd);
  • Statement stmt = c.createStatment
  • stmt.executeQuery 拿结果

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

  1. public Properties parseURL(String url, Properties defaults) throws java.sql.SQLException { 
  2.         Properties urlProps = (defaults != null) ? new Properties(defaults) : new Properties(); 
  3.         if (url == null) { 
  4.             return null; 
  5.         } 
  6.         if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX) 
  7.                 && !StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) { 
  8.             return null; 
  9.         } 
  10.         int beginningOfSlashes = url.indexOf("//"); 
  11.         if (StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)) { 
  12.             urlProps.setProperty("socketFactory", "com.mysql.management.driverlaunched.ServerLauncherSocketFactory"); 
  13.         } 

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

  1. DriverManager.getConnection(xx,xx,xx) 这个方法最终会调用 Service Provider 已经加载的 Driver中可用的driver,调用driver的getConnection方法,对应到Mysql的源码,就是下方这个,重点是`com.mysql.jdbc.ConnectionImpl.getInstance` 
  2.  
  3. public java.sql.Connection connect(String url, Properties info) { 
  4.         if (url == null) { 
  5.             throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.1"), SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null); 
  6.         } 
  7.         if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) { 
  8.             return connectLoadBalanced(url, info); 
  9.         } else if (StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) { 
  10.             return connectReplicationConnection(url, info); 
  11.         } 
  12.         Properties props = null
  13.         if ((props = parseURL(url, info)) == null) { 
  14.             return null; 
  15.         } 
  16.         if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) { 
  17.             return connectFailover(url, info); 
  18.         } 
  19.         try { 
  20.             Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url); 
  21.             return newConn; 

再来看 getInstance具体做了啥?

  1. protected static Connection getInstance(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) 
  2.             throws SQLException { 
  3.         if (!Util.isJdbc4()) { 
  4.             return new ConnectionImpl(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url); 
  5.         } 
  6.         return (Connection) Util.handleNewInstance(JDBC_4_CONNECTION_CTOR, 
  7.                 new Object[] { hostToConnectTo, Integer.valueOf(portToConnectTo), info, databaseToConnectTo, url }, null); 
  8.     } 
  1. this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), getProxy(), getSocketTimeout(), 
  2.                 this.largeRowSizeThreshold.getValueAsInt()); 
  3. this.io.doHandshake(this.user, this.password, this.database); 

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

  1. // save last exception to propagate to caller if connection fails 
  2.                 SocketException lastException = null
  3.                 // Need to loop through all possible addresses. Name lookup may return multiple addresses including IPv4 and IPv6 addresses. Some versions of 
  4.                 // MySQL don't listen on the IPv6 address so we try all addresses. 
  5.                 for (int i = 0; i < possibleAddresses.length; i++) { 
  6.                     try { 
  7.                         this.rawSocket = createSocket(props); // 这里创建了一个空的Socket对象 
  8.                         configureSocket(this.rawSocket, props); //将一些超时之类的属性设置到socket中 
  9.                         InetSocketAddress sockAddr = new InetSocketAddress(possibleAddresses[i], this.port); //获取host对应的ip地址等,再加上端口,组成一个Address 
  10.                         // bind to the local port if not using the ephemeral port 
  11.                         if (localSockAddr != null) { 
  12.                             this.rawSocket.bind(localSockAddr); 
  13.                         } 
  14.                         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 时的调用栈。

 

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

 

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

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

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

 

  1. ResultSetInternalMethods rs = readAllResults(callingStatement, maxRows, resultSetType, resultSetConcurrency, streamResults, catalog, resultPacket, 
  2.                   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 都支持全集指令,导致问题。之后改了一个检查方式解决了报警问题。

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

【本文为51CTO专栏作者“侯树成”的原创稿件,转载请通过作者微信公众号『Tomcat那些事儿』获取授权】

戳这里,看该作者更多好文

责任编辑:赵宁宁 来源: 51CTO专栏
相关推荐

2018-05-02 09:38:02

程序员代码互联网

2019-03-08 10:08:41

网络程序猿代码

2023-02-26 00:00:02

字符串分割String

2019-08-19 09:21:36

程序员Bug代码

2018-06-27 14:23:38

机器学习人工智能入门方法

2021-01-27 18:13:35

日志nginx信息

2016-03-04 14:14:02

电话免费越洋

2017-12-26 15:41:26

2018-12-26 09:44:02

分布式缓存本地缓存

2024-01-04 12:33:17

ChatGPTAI视频

2022-03-21 08:55:53

RocketMQ客户端过滤机制

2017-02-09 17:00:00

iOSSwiftKVC

2012-12-28 13:47:36

Raspberry PGeek

2022-01-05 17:13:28

监控HTTPS网站

2020-08-14 08:19:25

Shell命令行数据

2021-11-29 05:37:24

Windows Def操作系统微软

2009-04-28 07:48:29

盖茨打工基金会

2018-07-10 09:07:57

AI数据科技

2023-09-07 06:48:38

Intel显卡AMD

2018-10-22 15:29:50

点赞
收藏

51CTO技术栈公众号