暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

一文读懂JDBC

码农的日常记录 2018-11-13
330

一、背景

在开发业务系统时,经常需要对数据进行一些持久化操作,比如对数据进行新增、查询、更新和删除。这时候就需要使用数据库,通常使用第三方数据库,应用程序只需要调用数据库中间件提供的api即可。由于市面上存在很多数据库,如果提供的api不同,那么应用程序在使用不同的数据库时需要写不同的代码,带来维护的复杂性。这时候就需要一套标准,由不同的数据库厂家来共同这套遵守标准来提供实现给应用程序调用,JDBC(Java Database Connectivity)便是这样一套标准。

二、api简述

JDBC标准主要提供一套api规范,第三方应用通过实现JDBC的接口并提供一个jar包给应用程序调用,架构如下图

  1. 其中DriverManager负责加载各种不同驱动程序(Driver),并根据不同的请求,向调用者返回相应的数据库连接(Connection)。

  2. 第三方数据库中间件需要实现Driver接口提供给DriverManager

  3. 应用程序通过调用JDBC API 来实现对数据库的操作,第三方数据库需要提供各自api的实现,其中主要包括:

  4.     Connection:数据库连接,负责进行与数据库间的通讯,SQL执行以及事务处理都是在某个特定Connection环境中进行的。可以产生用以执行SQL的Statement。

        Statement:用以执行SQL查询和更新(针对静态SQL语句和单次执行)。

JDBC把对数据库的操作抽象出来,把建立数据库连接的行为聚合到Connection接口,对数据库操作的行为聚合到Statement接口中。

栗子:比如调用mysql提供的mysql-conector-java.jar进行查询:

  1. public class MysqlJdbcTest {

  2.    @Test

  3.    public void jdbcQuery() throws Exception {

  4.        PooledDataSource ds = new PooledDataSource ();

  5.        // 设置mysql driver

  6.        ds.setDriver ("com.mysql.jdbc.Driver");

  7.        // 设置数据库url

  8.        ds.setUrl ("jdbc:mysql://localhost:3306/test");

  9.        ds.setUsername ("root");

  10.        ds.setPassword ("root");

  11.        // 获取一个Connection

  12.        Connection con = ds.getConnection ();

  13.        // 执行查询

  14.        exexuteQuery (con);

  15.        con.close ();

  16.    }


  17.    private static ResultSet exexuteQuery(Connection con) throws SQLException {

  18.        PreparedStatement st = con.prepareStatement ("select * from udp_record");

  19.        ResultSet rs = st.executeQuery ();

  20.        return rs;

  21.    }

  22. }

三、数据库连接网络分析

上面的代码通过获取一个mysql的connection来进行查询,那么这个过程是如何实现的?为什么说数据库的连接是很耗费资源的呢?带着这个问题,使用wireshark试着抓一个本地的包来看一下这个调用过程,可以看到底层通过TCP协议+mysql二进制协议来执行,具体过程如下:

  1. 首先进行三次握手

  2. 接着发送一个Login Request

  3. 返回OK后进行query请求,看到一个query执行了7次request请求

  4. 最后发送一个RST 信号给服务器来中断连接 统计一下这条查询语句的时间为5.657848-5.496280=0.161568秒,如果使用一个线程来执行sql语句,假设2W日活,单用户单日平均执行10次查询,那么调用时间居然为9.976小时。如果使用多线程可以显著提高性能,同时使用线程池技术,来保证资源的合理利用(可以参考文章 线程池的前世今生

比较好奇的是mysql是如果实现connection,并把sql语句发送给中间件的,这时候来跟一下源码: 首先构建prepareStatement:

  1. PreparedStatement st = con.prepareStatement ("select * from udp_record");

其中包括对ResultType校验,如果带有动态参数,还会对参数进行校验,并把sql转为nativeSql(作为mysql缓存主键),和statementComment(用于执行数据库操作)

接下来调用statement的executeQuery()方法: 可以看到生成一个sendPacket缓冲区,

然后发送数据包给中间件,如果包含分页则设置分页参数

上面通过一条查询指令了解了对数据库的调用过程,那么如果有多个数据库中间件,他们是如何注册到JDBC的DriverManager中,又是如何获取到对应数据库的connection呢?

四、DriverManager实现机制

跟踪之前获取connection的代码,看到Conenection实际是从DriverManager中获得的

接下来跟进DriverManager ,看到静态语句块:

其中loadInitialDrivers()方法如下:

这段代码实际上使用spi机制 获取META-INF包含的Driver实现类,并通过Iterater.next()把类加载到ClassLoader来执行其中的静态方法,

比如mysql 提供的 Driver实现类的静态代码块如下, 也就是调用DriverManager的registerDriver方法:

       获取Connection对象时,通过迭代DriverManager中的registerDrivers ,分别对url前缀进行判断,如果满足条件则调用Driver获取Connection对象,比如在Postgresql提供的JDBC中的判断条件为:

      那么我们之前栗子中设置的url为:"jdbc:mysql://localhost:3306/test",根据前缀获取到的就是mysql的connection实现类。

通过上面分析,我们知道了需要调用jdbc的connection接口来对数据库进行连接,通过statement接口来进行增删改查,但是对数据进行处理还需要考虑数据的事务性,同时sql语句也不希望直接硬编码到代码中,接下来通过mybatis源码来进一步了解。

更多技术文章,请关注微信公众号:


文章转载自码农的日常记录,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论