走进JavaWeb技术世界3JDBC的进化与连接池技术

41次阅读

共计 5138 个字符,预计需要花费 13 分钟才能阅读完成。

本系列文章将整理到我在 GitHub 上的《Java 面试指南》仓库,更多精彩内容请到我的仓库里查看

https://github.com/h2pl/Java-…

喜欢的话麻烦点下 Star 哈

文章首发于我的个人博客:

www.how2playlife.com

本文是微信公众号【Java 技术江湖】的《走进 JavaWeb 技术世界》其中一篇,本文部分内容来源于网络,为了把本文主题讲得清晰透彻,也整合了很多我认为不错的技术博客内容,引用其中了一些比较好的博客文章,如有侵权,请联系作者。
该系列博文会告诉你如何从入门到进阶,从 servlet 到框架,从 ssm 再到 SpringBoot,一步步地学习 JavaWeb 基础知识,并上手进行实战,接着了解 JavaWeb 项目中经常要使用的技术和组件,包括日志组件、Maven、Junit,等等内容,以便让你更完整地了解整个 Java Web 技术体系,形成自己的知识框架。为了更好地总结和检验你的学习成果,本系列文章也会提供每个知识点对应的面试题以及参考答案。

如果对本系列文章有什么建议,或者是有什么疑问的话,也可以关注公众号【Java 技术江湖】联系作者,欢迎你参与本系列博文的创作和修订。

<!– more –>

JDBC 数据库连接池

谈谈连接池、线程池技术原理

做互联网研发,最早接触使用 jdbc 技术,为了数据库连接能够复用,会用到 c3p0、dbcp 等数据库连接池。应该是研发人员最早接触的数据库连接池,再到 httpclient http 连接池,再到微服务 netty 连接池,redis 客户端连接池,以及 jdk 中线程池技术。

       这么多数据库、http、netty 连接池,jdk 线程池,本质上都是连接池技术,连接池技术核心是连接或者说创建的资源复用。

       连接池技术核心:通过减少对于连接创建、关闭来提升性能。用于用户后续使用,好处是后续使用不用在创建连接以及线程,因为这些都需要相关很多文件、连接资源、操作系统内核资源支持来完成构建,会消耗大量资源,并且创建、关闭会消耗应用程序大量性能。

       网络连接本身会消耗大量内核资源,在 linux 系统下,网络连接创建本身 tcp/ip 协议栈在内核里面,连接创建关闭会消耗大量文件句柄(linux 中万物皆文件,一种厉害抽象手段)系统资源。当下更多是应用 tcp 技术完成网络传输,反复打开关闭,需要操作系统维护大量 tcp 协议栈状态。

       连接池本质上是构建一个容器,容器来存储创建好的线程、http 连接、数据库连接、netty 连接等。对于使用方相当于黑盒,按照接口进行使用就可以了。各个连接池构建、使用管理详细过程大概分成以下三部分。

       第一部分:首先初始化连接池,根据设置相应参数,连接池大小、核心线程数、核心连接数等参数,初始化创建数据库、http、netty 连接以及 jdk 线程。

       第二部分:连接池使用,前边初始化好的连接池、线程池,直接从连接池、线程中取出资源即可进行使用,使用完后要记得交还连接池、线程池,通过池容器来对资源进行管理。

       第三部分:对于连接池维护,连接池、线程池来维护连接、线程状态,不可用连接、线程进行销毁,正在使用连接、线程进行状态标注,连接、线程不够后并且少于设置最大连接、线程数,要进行新连接、线程创建。

       通过上边可以了解到各种连接池技术以及线程池原理或者说套路,理解原理才能不被纷繁复杂表象掩盖。

       下面谈谈构建自己连接池,其实理解了连接池、线程原理,可以使用 ArrayList 来构建自己连接池、线程池。初始化时创建配置连接数、线程,存储在 ArrayList 容器中,使用时从 ArrayList 从取出连接、线程进行使用,执行完任务后,提交回 ArrayList 容器。前提条件是单线程,在多线程状态下要用线程安全容器。

       前边根据原理介绍了一个简单连接池、线程池怎样构建,实际工业级别线程池还要考虑到连接状态,短连接重连,线程池维护管理高效,线程池稳定等多个因素。

       需要用到连接池而又没有相关开源产品可用时,java 连接池可以使用 common-pool2 来构建,比如 google 开源 gRPC 技术,本身是高性能跨平台技术,但目前作为微服务使用,没有连接池、负载均衡等相应配套,这时可以根据需要自己基于 Java 容器构建自己连接池。也可以利用 common-pool2 构建连接池来提升应用性能,以及保持高可用。common-pool2 本身不仅仅可以构建连接池使用,还可以用来构建对象池。

       连接池还有一个副作用就是实现了高可用,在微服务场景下一个连接不可用,那么再从 netty 连接池中取出一个进行使用,避免了连接不可用问题。

       掌握原理从比较全面掌握各种池技术,避免数据库连接池,再到 httpclient http 连接池,再到微服务 netty 连接池,redis 客户端连接池,以及 jdk 中线程池,对象池各种各样池技术,使我们眼花缭乱,花费过多时间,掌握原理机制以不变应万变。

       推广一下这个方法,其他技术也是类似,深入掌握其原理,就可以明白其他类似技术相似原理,避免疲于应对各种新技术。但每一种架构设计与实现又与领域有着关系,也不可讲原理不顾实际情况扩展。理论与架构设计、源码学习相结合才是最好的,希望有帮助。

JDBC 数据库连接池 

转自:

什么情况下使用连接池?

对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。但是对于一个复杂的数据库应用,情况就完全不同了。频繁的建立、关闭连接,会极大的减低系统的性能,因为对于连接的使用成了系统性能的瓶颈。

使用连接池的好处

  1. 连接复用。通过建立一个数据库连接池以及一套连接使用管理策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。
  2. 对于共享资源,有一个很著名的设计模式:资源池。该模式正是为了解决资源频繁分配、释放所造成的问题的。把该模式应用到数据库连接管理领域,就是建立一个数据库连接池,提供一套高效的连接分配、使用策略,最终目标是实现连接的高效、安全的复用。

连接池的实现

数据库连接池的基本原理是在内部对象池中维护一定数量的数据库连接,并对外暴露数据库连接获取和返回方法。

外部使用者可通过 getConnection 方法获取连接,使用完毕后再通过 close 方法将连接返回,注意此时连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。

Java 中有一个 DataSource 接口, 数据库连接池就是 DataSource 的一个实现

常用数据库连接池

  • Apache DBCP

    • 官网
  • C3P0

    • 官网
  • Druid

    • GitHub

一、JDBC 数据库连接池的必要性

在使用开发基于数据库的 web 程序时,传统的模式基本是按以下步骤:

①在主程序(如 servlet、beans)中建立数据库连接。
②进行 sql 操作
③断开数据库连接。

这种模式开发,存在的问题:

①普通的 JDBC 数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中,再验证用户名和密码(得花费 0.05s~1s 的时间)。需要数据库连接的时候,就向数据库要求一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。数据库的连接资源并没有得到很好的重复利用. 若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会造成服务器的崩溃。
②对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库。
③这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。

二、数据库连接池(connection pool)

数据库连接池简单介绍

为解决传统开发中的数据库连接问题,可以采用数据库连接池技术。

数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

数据库连接池工作原理:

数据库连接池技术的优点

资源重用:

①由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增加了系统运行环境的平稳性。

更快的系统反应速度:
数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间

新的资源分配手段:
对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源

统一的连接管理,避免数据库连接泄露:
在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄露

三、两种开源的数据库连接池

JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器 (Weblogic, WebSphere, Tomcat) 提供实现,也有一些开源组织提供实现:
①DBCP 数据库连接池
②C3P0 数据库连接池

DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource 称为连接池

数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。

当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但上面的代码并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。

参考文章

https://blog.csdn.net/u010028…
https://www.cnblogs.com/shaox…
https://www.cnblogs.com/alber…
https://blog.csdn.net/weixin_…
https://www.jianshu.com/p/073…

微信公众号

个人公众号:黄小斜

黄小斜是跨考软件工程的 985 硕士,自学 Java 两年,拿到了 BAT 等近十家大厂 offer,从技术小白成长为阿里工程师。

作者专注于 JAVA 后端技术栈,热衷于分享程序员干货、学习经验、求职心得和程序人生,目前黄小斜的 CSDN 博客有百万 + 访问量,知乎粉丝 2W+,全网已有 10W+ 读者。

黄小斜是一个斜杠青年,坚持学习和写作,相信终身学习的力量,希望和更多的程序员交朋友,一起进步和成长!

原创电子书:
关注公众号【黄小斜】后回复【原创电子书】即可领取我原创的电子书《菜鸟程序员修炼手册:从技术小白到阿里巴巴 Java 工程师》

程序员 3T 技术学习资源: 一些程序员学习技术的资源大礼包,关注公众号后,后台回复关键字 “资料” 即可免费无套路获取。

考研复习资料:
计算机考研大礼包,都是我自己考研复习时用的一些复习资料, 包括公共课和专业的复习视频,这里也推荐给大家,关注公众号后,后台回复关键字 “考研” 即可免费获取。

技术公众号:Java 技术江湖

如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号【Java 技术江湖】一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点 Docker、ELK,同时也分享技术干货和学习经验,致力于 Java 全栈开发!

Java 工程师必备学习资源: 一些 Java 工程师常用学习资源,关注公众号后,后台回复关键字 “Java” 即可免费无套路获取。

正文完
 0

走进JavaWeb技术世界3JDBC的进化与连接池技术

41次阅读

共计 19700 个字符,预计需要花费 50 分钟才能阅读完成。

走进 JavaWeb 技术世界 3:JDBC 的进化与连接池技术

微信公众号【黄小斜】大厂程序员,互联网行业新知,终身学习践行者。关注后回复「Java」、「Python」、「C++」、「大数据」、「机器学习」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「笔试」、「面试」、「面经」、「计算机基础」、「LeetCode」等关键字可以获取对应的免费学习资料。

                     

网络访问

随着 Oracle, Sybase, SQL Server ,DB2,  Mysql 等人陆陆续续住进数据库村,这里呈现出一片兴旺发达的景象,无数的程序在村里忙忙碌碌,读写数据库,实际上一个村落已经容不下这么多人了,数据库村变成了数据镇。

这一天,数据库镇发生了一件大事:它连上了网络!

外部的花花世界一下全部打开,很多程序开始离开这个拥挤的城镇,住到更加宜居的地方去。

可是他们的工作还是要读写数据库,大家都在想办法能不能通过网络来访问数据库镇的数据库。

其中移居到 Tomcat 村的 Java 最为活跃,这小子先去拜访了一下 Mysql ,    相对于 Oracle, Sybase 等大佬,Mysql 还很弱小,也许容易搞定。

Java 说:“Mysql 先生,现在已经网络时代了,您也得与时俱进啊,给我们开放下网络接口吧。”

Mysql  说:“还网络时代,你们这些家伙越来越懒了,都不愿意到我们家里来了!说说吧,你想怎么开放?”

Java 说:“很简单,您听说过 TCP/IP 还有 Socket 没有?没有吗?!没关系,您的操作系统肯定知道,它内置实现了 TCP/IP 和 socket,   您只需要和他商量一下,需要申请一个 ip,确定一个端口,然后您在这个端口监听,我每次想访问数据了,就会创建一个 socket,向你发起连接请求,你接受了就行了。”

(刘欣注:参见《张大胖的 socket》)

“这么麻烦啊?”

“其实也简单,您的操作系统会帮忙的,他非常熟悉,再说只需要做一次就行,把这个网络访问建立起来,到时候很多程序都会来访问您,您会发财的。”

“不会这么简单吧,假设说,我是说假设啊,通过 socket 我们建立了连接,通过这个连接,你给我发送什么东西?我又给你发什么东西?”Mysql 非常老练,直击命门。

“呃,这个 ….”

Java 其实心里其实非常明白,这需要 和 Mysql 定义一个应用层的协议,就是所谓的你发什么请求,我给你什么响应

例如:

客户端程序先给 Mysql 打个招呼,Mysql 也回应一下,俗称握手。

怎么做认证、授权,数据加密,数据包分组。

用什么格式发送查询语句,用什么格式来发送结果。

如果结果集很大,要一下子全发过来吗?

怎么做数据缓冲?

……

等等一系列让人头痛的问题。

本来 Java 是想独自定义,这样自己也许能占点便宜,没想到 Mysql  直接提出来了。

“这样吧”Java 说“我们先把这个应用层的协议定义下来,然后您去找操作系统来搞定 socket 如何?”

“这还差不多”。Mysql 同意了。

两人忙活了一星期,才把这个应用层协议给定义好。

然后又忙了一星期,才把 Mysql 这里的 socket 搞定。

Java 赶紧回到 Tomcat 村,做了一个实验:通过 socket 和 mysql 建立连接,然后通过 socket 发送约定好的应用层协议,还真不错,一次都调通了,看来准备工作很重要啊。

(刘欣注:这是我的杜撰,mysql 的网络访问早就有了,并不是 java 捷足先登搞出来的)

统一接口

搞定了 Mysql ,  Java 很得意,这是一个很好的起点,以后和 Oracle, SQL Server, Db2 等大佬谈判也有底气了。

尤其是和 mysql 商量出的应用层协议,mysql 也大度的公开了,这样一来,不管是什么语言写的程序,管你是 java, pyhton, ruby , php……   只要能使用 socket,   就可以遵照 mysql 的应用层协议进行访问,mysql 的顾客呈指数级增长,财源滚滚。尤其是一个叫 PHP 的家伙,简直和 mysql 成了死党。

Oracle, Db2 那帮大佬一看,立刻就红了眼,不到 Java 去谈判,也迫不及待的定义了一套属于自己的应用层访问协议。

令人抓狂的是,他们的网络访问协议和 Msyql 的完全不一样!这就意味着之前写的针对 mysql 的程序无法针对 Oracle , Db2 通用,如果想切换数据库,每个程序都得另起炉灶写一套代码!

更让人恶心的是,每套代码都得处理非常多的协议细节,   每个使用 Java 进行数据库访问的程序都在喋喋不休的抱怨:我就想通过网络给数据库发送 SQL 语句,怎么搞的这么麻烦?

原因很简单,就是 直接使用 socket 编程,太 low 了,必须得有一个抽象层屏蔽这些细节!

Java 开始苦苦思索,做出一个好的抽象不是那么容易的。

首先得有一个叫连接 (Connection) 的东西,用来代表和数据库的连接。

想执行 SQL 怎么办?用一个 Statement 来 表示吧。SQL 返回的结果也得有个抽象的概念:ResultSet。

他们之间的关系如图所示:

从 Connection 可以创建 Statement,    Statement 执行查询可以得到 ResultSet。

ResultSet 提供了对数据进行遍历的方法,就是 rs.next(),rs.getXXXX ….      完美!

对了,无论是 Connection, 还是 Statement, ResultSet,他们都应该是接口,而不能是具体实现。

具体的实现需要由各个数据库或者第三方来提供,毫无疑问,具体的实现代码中就需要处理那些烦人的细节了!

Java 把这个东西叫做 JDBC,想着自己定义了一个标准接口,把包袱都甩给你别人,他非常得意。

面向接口编程

第一个使用 JDBC,叫做学生信息管理的程序很快发现了问题,跑来质问 Java:“你这个 Connection 接口设计的有问题!”

Java 说:“不可能,我的设计多完善啊!”

“看来你这个规范的制定者没有真正使用啊,你看看,我想连接 Mysql, 把 Mysql 提供的 jdbc 实现 (mysql-connector-java-4.1.jar ) 拿了过来,建立一个 Connection :”

“这不是挺正常的吗?你要连接 Mysql , 肯定要提供 ip 地址,端口号,数据库名啊”Java 问到。

“问题不在这里,昨天我遇到 mysql 了,他给了我一个号称性能更强劲的升级版 mysql-connector-java-5.0.jar,我升级以后,发现我的代码编译都通不过了,原来 mysql 把 MysqlConnectionImpl 这个类名改成了 MysqlConnectionJDBC4Impl,你看看,你整天吹嘘着 要面向接口编程,不要面向实现编程,但是你自己设计的东西都做不到啊”

Java 觉得背上开始出汗,那个程序说的没错,设计上出了漏洞,赶紧弥补吧。

既然不能直接去 new 一个 Connection 的实现,肯定要通过一个新的抽象层来做,这个中间层叫做什么?

Java 想到了电脑上的驱动程序,很多硬件没法直接使用,除非安装了驱动。那我也模拟一下再做一个抽象层吧:Driver

每个数据库都需要实现 Driver 接口,通过 Driver 可以获得数据库连接 Connection,但是这个 Driver 怎么才能 new 出来呢?肯定不能直接 new ,    Java 似乎陷入了鸡生蛋、蛋生鸡的无限循环中了。

最后,还是 Java 的反射机制救了他,不要直接 new 了,每个数据库的 Driver 都用一个文本字符串来表示,运行时动态的去装载,例如 mysql 的 Driver 是这样的:

Oracle 是这样的:

只要这个 Driver Class 不改动,其他具体的实现像 Connection,  Statement, ResultSet 想怎么改就怎么改。

接下来的问题是同一个程序可能访问不同的数据库,可能有多个不同 Driver 都被动态装载进来,如何来管理?

那就搞一个 DriverManager 吧,Mysql 的 Driver, Oracle 的 Driver 在类初始化的时候,一定得注册到 DriverManager 中来,这样 DriverManager 才能管理啊:

注意:关键点是 static 的代码块,在一个类被装载后就会执行。

DriverManager 可以提供一个 getConnection 的方法,用于建立数据库 Connection。

DriverManager 会把这些信息传递给每个 Driver,让每个 Driver 去创建 Connection。

慢着!如果 DriverManager  里既有 MysqlDriver, 又有 OracleDriver ,  这里到底该连接哪一个数据库呢?难道让两个 Driver 都尝试一下?那样太费劲了吧,还得区分开,没法子只好让那些程序在数据库连接 url 中来指定吧:

url 中指明了这是一个什么数据库,每个 Driver 都需要判断下是不是属于自己支持的类型,是的话就开始连接,不是的话直接放弃。

(刘欣注:每个 Driver 接口的实现类都需要实现一个 acceptsURL(Sting url)方法,判断这个 url 是不是自己能支持的。)

唉,真是不容易啊,Java 想,这下整个体系就完备了吧,为了获得一个 Connection , 综合起来其实就这么几行代码:

无论是任何数据库,只要正确实现了 Driver,Connection 等接口,就可以轻松的纳入到 JDBC 框架下了。

Java 终于可以高兴的宣布:“JDBC 正式诞生了!”

(完)

连接,连接,总是连接!

生活中肯定有比数据库连接更有趣的事情。

1

数据库连接

又到了数据库连接的时间!

那些码农把数据库参数送过来,Oracle , Db2,  Sybase, SQL Server 这些 JDBC Driver 懒洋洋起来去干活赚钱。

小东也是其中之一,  每天的工作就是连接 Mysql 数据库,发出 SQL 查询,获取结果集。

工作稳定,收入不菲,只是日复一日,年复一年,枯燥的工作实在是太令人厌烦了。

有时候小东会和其他 JDBC Driver 聊天,谈起有些不着调的码农,创建一个 Connection,  发出一个查询,处理完 ResultSet 后,立刻就把 Connection 给关掉了。

“他们简直不知道我们建立一个数据连接有多么辛苦,先通过 Socket 建立 TCP 连接,然后还要有应用层的握手协议,唉,不知道要干多少脏活累活,这帮码农用完就关,真是浪费啊。”

“还有更可气的,有些家伙使用了 PreparedStatement , 我通知数据库做了预编译,制定了查询计划,为此我还花费了不菲的小费。但是只执行了一次查询,Connection 就关掉了,PreparedStatement 也就不可用了,现在数据库都懒的给我做预编译了!”

“你们这都是好的,有些极品根本就不关闭 Connection,  最后让这个 Connection 进入了不可控状态。”

“我们啊,都把宝贵的生命都献给了数据库连接事业 ……”

抱怨归抱怨,大部分人都安于现状,逆来顺受了。

2

向 Tomcat 取经

但是不安分的小东决心改变,他四处拜访取经,但是一无所获。

这一天在 Tomcat 村遇到了 Tomcat 村长,看到了村长处理 Http 请求的方式,突然间看到了曙光。

村长说:我们本来是一个线程处理一个 Http 请求,一个 Http 请求来到我们这里以后,我并不会新建一个线程来处理,而是从一个小黑屋叫来一个线程直接干活,干完活以后再回到小黑屋待着。

小东问:小黑屋是什么?

(码农翻身注:参见文章《我是一个线程》)

村长说:“学名是线程池,为了充分利用资源,我在启动时就建立了一批线程,放到线程池里,需要线程时直接去取就可以了。”

“那要是线程池里的线程都被派出去了怎么办 ?”

“ 要么新创建线程,要么新来的 Http 请求就要等待了。实际上,线程也不是无限可用的资源,也得复用。”

小东心想,我们 JDBC 也可以这么搞啊,把数据库连接给缓存下来,随用随取,一来正好可以控制码农们无限制的连接数据库;二来可以减少数据库连接时间;第三还可以复用 Connection 上的 PreparedStatement, 不用老是找数据库预编译了。

3

数据库连接池

建立数据库连接池不是那么一帆风顺的,小东的第一次尝试是创建了一个 ConnectionPool 这个接口:

里边有两个重要的方法,getConnection(), 用于从池中取出一个让用户使用;

releaseConnection() 把数据库连接放回池中去。

小东想,只要我写一个 ConnectionPool 的实现类,里边可以维护一个管理数据库连接的数据结构就行了,码农们用起来也很方便,他们肯定会喜欢的。

可是事与愿违,几乎没有人用这样的接口。

小东经过多方打探才明白,码农们要么是用 DriverManager 来获得 Connection,要么是使用 DataSource 来得到 Connection;关闭的时候,只需要调用 Connection.close() 就可以了。

这样的代码已经有很多了,而小东的新方案相当于新的接口,需要改代码才能用,话说回来,谁愿意没事改代码玩?尤其是正在运行的系统。

再做一次改进吧,小东 去找 Java 这个设计高手求教。

Java 说:“虽然 ConnectionPool 概念不错,但是具体的实现最好隐藏起来,对码农来说,还是通过 DataSource 来获取 Connection,   至于这个 Connection 是新建的还是从连接池中来的,码农不应该关心,所以应该加一个代理 Proxy,把物理的 Connection 给封装起来,然后把这个 Proxy 返回给码农。”

“那这个 Proxy 是不是得和您定义的接口 Connection 接口保持一致?要不然码农还得面对新东西。”

“是的,这个 Proxy 应该也实现 JDBC 的 Connection 接口,像这样:”

(点击看大图)

小东说:”奥,我明白了,当码农从 DataSource 中获得 Connection 的时候,我返回的其实是一个 ConnectionProxy ,   其中封装了一个从 ConnectionPool 来的 Connection ,  然后把各种调用转发到这个实际的 physicalConn 的方法去,关键点在 close,  并不会真的关闭 Connection, 而是放回到 ConnectionPool“

“哈哈,看来你已经 get 了,这就是面向接口编程的好处啊,你给码农返回了一个 ConnectionProxy, 但是码农们一无所知,仍然以为是在使用 Connection , 这是一次成功的‘欺骗’啊”

“但是你定义的 Connection 接口中的方法实在是太多了,足足有 50 多个,我这个 Proxy 类实际上只关注那么几个,例如 close 方法,其他的都是转发而已,这么多无聊的转发代码是在是太烦人了”

Java 说:“还有一种办法,可以用动态代理啊”

小东问:“什么是动态代理?”

“ 刚才我们提供的那个 Proxy 可以称为静态代理,我的动态代理不用你写一个类去实现 Connection, 完全可以在运行期来做,还是先来看代码吧 ”

(点击看大图)

“代码有点难懂,你看,这里没有声明一个实际的类来实现 Connection 接口,而是用动态代理在运行时创建了一个类 Proxy.newProxyInstance(….) ,    重点部分就是 InvocationHandler,  在他的 invoke 方法中,我们判断下被调用的方法是不是 close, 如果是,就把 Connection 放回连接池,如果不是,就调用实际 Connection 的方法。”Java 解释了一通。

小东惊叹到:“代码虽然难懂,但是精简了好多,我对 Java 反射不太熟,回头我再仔细研究下。”

(码农翻身注:不熟悉 Java 动态代理的也可以研究下,这是一项重要的技术)

经过一番折腾,数据库连接池终于隐藏起来了,码农们可以使用原有的方式去获取 Connection, 只是不知道背后其实发生了巨变。

当然也不可能让码农完全意识不到连接池,毕竟他们还得去设置一些参数,小东定义了一些:

数据库连接池获得了巨大的成功,几乎成了每一个 Java Web 项目的标配,不一样的 JDBC 驱动小东也获得了极高的荣誉,后面等着他的还会有哪些挑战呢?

抱怨

JDBC 出现以后,以其对数据库访问出色的抽象,良好的封装,特别是把接口和具体分开的做法,赢得了一片称赞。

(参见文章《》)

乘着 Java 和互联网的东风,JDBC 在风口飞了起来,无数的程序开始使用 JDBC 进行编程,访问数据库村的数据库,在数据库村,无论是大佬 Oracle, 还是小弟 Mysql 都赚的盆满钵满。

所谓物极必反,JDBC 的代码写得多了,它的弱点就暴露出来了,很多码农抱怨道:“JDBC 是在是太 Low 了”。

消息传到 Tomcat 村,Java 简直不相信自己的耳朵:“什么?JDBC 还很 low ? 看来那帮码农没有用 socket 访问过数据库吧?!那才叫 low .”

Tomcat 说:“你是个标准的制定者,写代码太少了,太不接地气了,你看看这样的代码:”

Java 把代码拿过来一看,不由的倒吸了一口凉气:“代码怎么这么长啊,似乎是有那么一点问题,‘噪声’似乎太多了,把业务代码全给淹没了”

Tomcat 说:”看来你也是个明白人啊,为了正确的打开和关闭你定义的 Connection , Statement, ResultSet 需要花很多功夫,再加上那些异常处理,一个 50 多行的程序,真正做事的也就那么 10 几行而已,这些琐碎代码太烦人了,所以大家抱怨很 low 啊。”

Java 表示同意:“不错,可以想象,如果代码中有大量这样的代码,码农会抓狂的,不过,”Java 突然想到了些什么,“其实这不是我的问题,码农们抱怨错人了,我作为一门语言,所能提供的就是贴近底层 (socket) 的抽象,这样通用性最强。至于码农想消除这些重复代码,完全可以再封装,再抽象,再分层啊

Tomcat 想想也是,在计算机世界里,每个人都有分工,不能强求别人做不喜欢也不擅长的事情,看来这件事错怪 Java 了。

JDBCTemplate

Java 预料的不错,稍微有点追求,不愿意写重复代码的码农都对 JDBC 做了封装,例如写个 DBUtil 的工具类把打开数据库连接,发出查询语句都封装了起来。

码农的抱怨也渐渐平息了。

有一天,有个叫 JDBCTemplate 的人来到了 Tomcat 村找到了 Java , 他自称是 Rod Johnson 派来专门用于解决 JDBC 问题的,他提供了一个优雅而简洁的解决方案。

(注:Rod Johnson 就是 Spring 最初的作者)

JDBCTemplate 说:“尊敬的 Java 先生,感谢您发明了 JDBC,让我们可以远程访问数据库村,您也听说了不少对 JDBC 的抱怨吧,我的主人 Rod Johnson 也抱怨过,不过他在大量的编程实践中总结了很多经验,他认为数据库访问无外乎这几件事情:

指定数据库连接参数

打开数据库连接

声明 SQL 语句

预编译并执行 SQL 语句

遍历查询结果

处理每一次遍历操作

处理抛出的任何异常

处理事务

关闭数据库连接”

“我的主人认为”JDBCTemplate 说,“开发人员只需要完成黑体字工作就可以了,剩下的事情由我来办“

“你们主人的总结能力很强,把一个框架应该做的事情和用户应该做的事情区分开了”Java 说

JDBCTemplate 看到 Java 态度不错,赶紧趁热打铁:“我给你看个例子:”

Java 和之前那个传统的 JDBC 比较了一下,JDBCTemplate 的方式的确是把注意力放到了业务层面:只关注 SQL 查询,以及把 ResultSet 和 User 业务类进行映射,至于如何打开 / 关闭 Connection, 如何发出查询,JDBCTemplate 在背后都给你悄悄的完成了,完全不用码农去操心。

“你在 JDBC 上做了不错的抽象和封装”Java 说,“但是我还不明白 JDBCTemplate 怎么创建出来的”

“这很简单,你可以直接把它 new 出来,当然 new 出来的时候需要一个参数,就是 javax.sql. DataSource,这也是你定义的一个标准接口啊”

“ 当然,我主人 Rod Johnson 推荐结合 Spring 来使用,可以轻松的把我‘注入’到各个你需要的地方去 ”。

“明白了,你们主人这是要推销 Spring 啊,那是什么东西?”

“简单的说,就是一个依赖注入和 AOP 框架,功能强大又灵活。具体的细节还得让我主人亲自来给您介绍了”

(注:参见《Spring 的本质系列 (1) — 依赖注入》返回上一级 回复数字 0002 阅读文章和《Spring 的本质系列 (2) — AOP》返回上一级 回复数字 0003 阅读文章)

“好吧,不管如何,我看你用起来还不错,可以向大家推荐一下。”

O/R Mapping

JDBCTemplate 这样对 JDBC 的封装,把数据库的访问向前推进了一大步,但是 Tomcat 村和数据库村的很多有识之士都意识到:本质的问题仍然没有解决!

这个问题就是面向对象世界和关系数据世界之间存在的巨大鸿沟。

Tomcat 村的 Java 程序都是面向对象的:封装、继承、多态,对象被创建起来以后,互相关联,在内存中形成了一张图。

数据库村的关系数据库则是表格:主键,外键,关系运算、范式、事务。数据被持久化在硬盘上。

ResultSet 依然是对一个表的数据的抽象和模拟:rs.next() 获取下一行,rs.getXXX(“XX”) 访问该行某一列。

把关系数据转化成 Java 对象的过程,仍然需要码农们写大量代码来完成。

现在码农的呼声越来越高,要把这个过程给自动化了。他们的要求很清晰:我们只想用面向对象的方式来编程,我们告诉你 Java 类、属性 和数据库表、字段之间的关系,你能不能自动的把对数据库的增删改查给实现了 

他们还把这个诉求起了一个很洋气的名称:O/R Mapping (Object Relational Mapping)

Java 自然不敢怠慢,赶紧召集 Tomcat 村和数据库村开了一次会议,确定了这么几个原则:

1. 数据库的表映射为 Java 的类(class)

2. 表中的行记录映射为一个个 Java 对象

3. 表中的列映射为 Java 对象的属性。

但是光有这几个原则是远远不够的,一旦涉及到实际编程,细节会扑面而来:

1. Java 类的粒度要精细的多,有时候多个类合在一起才能映射到一张表

例如下面的例子,User 类 的 name 属性其实是也是一个类,但在数据库 User 表中, firstName, middleName, lastName 却是在同一张表中的。

2. Java 的面向对象有继承,而数据库都是关系数据,根本没有继承这回事!

这时候可选的策略就很多了,比如

(1) 把父类和子类分别映射到各自的 Table 中,数据会出现冗余

(2) 把父类的公共属性放到一个 Table 中,每个子类都映射到各自的 Table 中,但是只存放子类自己的属性。子类和父类的表之间需要关联。

(3) 干脆把父类和子类都映射到同一张 Table 中,用一个字段(例如 Type)来表明这一行记录是什么类型。

3. 对象的标识问题

Java 中使用 a ==b 或者 a.equals(b) 来进行对象是否相等的判断,而数据库则是另外一套:使用主键。

4. 对象的关联问题

在 Java 中,一个对象关联到另外一个或者一组对象是在是太常见了,双向的关联(也就是 A 引用 B , B 反过来也引用了 A)也时常出现,而在数据库中定义关联能用的手段只剩下外键和关联表了。

5. 数据导航

在 OOP 中,多个对象组成了一张网,顺着网络上的路径,可以轻松的从一个对象到达另外一个对象。例如:City c = user.getAddress().getCity();

在关系数据库中非得通过 SQL 查询,表的连接等方式来实现不可。

6. 对象的状态

在 OOP 中,对象无非就是创建出来使用,如果不用了,就需要回收掉,但是一旦扯上数据库,势必要在编程中引入新的状态,例如“已经持久化”

……

(注:本来想讲 Hibernate, 但是限于篇幅,实在是无法展开讲细节,这几个问题是 Hibernate 官网上提到的,是 O /R Maping 的本质问题)

这些细节问题让 Java 头大,他暗自思忖:” 还是别管那些码农的抱怨,我还是守住 JDBC 这一亩三分地吧,这些烦人的 O /R Mapping 问题还是让别人去处理好了。“

O/R Mapping 的具体实现就这么被 Java 搁置下了。

Hibernate 和 JPA

随着时间的推移,各大厂商都想利用 Java 赚钱,联合起来搞了一个叫 J2EE 的规范,然后疯狂的推行自己的应用服务器(例如 Weblogic, Websphere 等等),还搭配着硬件销售,在互联网泡沫时代赚了不少钱。

J2EE 中也有一个叫 Entity Bean 的东西,试图去搞定 O /R Mapping , 但其蹩脚的实现方式被码农们骂了个狗血喷头。

转眼间,时间到了 2001 年,Tomcat 告诉 Java 说:“听说了吗?现在很多码农都被一个叫 Hibernate 的东西给迷住了”

“ 冬眠(Hibernate)?让内存中的数据在数据库里冬眠,这个名字起的很有意境啊,我猜是一个 O /R Mapping 工具吧 ”

“ 没错,是由一个叫 Gavin King 的小帅哥开发的,这个框架很强悍,它实现了我们之前讨论的各种烦人细节,大家都趋之若鹜,已经成为 O /R Mapping 事实上的标准,Entity Bean 已经快被大家抛弃了。“

“没关系,Entity Bean 从 1.0 开始就是一个扶不起的阿斗,我已经想通了,我这里只是指定标准,具体的实现让别人去做。既然 Hibernate 这么火爆,我们就把 Gavin King 招安了吧”

“怎么招安?”

“让小帅哥过来领导着大家搞一个规范吧,参考一下 Hibernate 的成功经验,他应该会很乐意的。”

不久以后,一个新的 Java 规范诞生了,专门用于处理 Java 对象的持久化问题,这个新的规范就是 JPA(Java Persistence API), Hibernate 自然也实现了这个规范,几乎就是 JPA 的首选了。

谈谈连接池、线程池技术原理

做互联网研发,最早接触使用 jdbc 技术,为了数据库连接能够复用,会用到 c3p0、dbcp 等数据库连接池。应该是研发人员最早接触的数据库连接池,再到 httpclient http 连接池,再到微服务 netty 连接池,redis 客户端连接池,以及 jdk 中线程池技术。

       这么多数据库、http、netty 连接池,jdk 线程池,本质上都是连接池技术,连接池技术核心是连接或者说创建的资源复用。

       连接池技术核心:通过减少对于连接创建、关闭来提升性能。用于用户后续使用,好处是后续使用不用在创建连接以及线程,因为这些都需要相关很多文件、连接资源、操作系统内核资源支持来完成构建,会消耗大量资源,并且创建、关闭会消耗应用程序大量性能。

       网络连接本身会消耗大量内核资源,在 linux 系统下,网络连接创建本身 tcp/ip 协议栈在内核里面,连接创建关闭会消耗大量文件句柄(linux 中万物皆文件,一种厉害抽象手段)系统资源。当下更多是应用 tcp 技术完成网络传输,反复打开关闭,需要操作系统维护大量 tcp 协议栈状态。

       连接池本质上是构建一个容器,容器来存储创建好的线程、http 连接、数据库连接、netty 连接等。对于使用方相当于黑盒,按照接口进行使用就可以了。各个连接池构建、使用管理详细过程大概分成以下三部分。

       第一部分:首先初始化连接池,根据设置相应参数,连接池大小、核心线程数、核心连接数等参数,初始化创建数据库、http、netty 连接以及 jdk 线程。

       第二部分:连接池使用,前边初始化好的连接池、线程池,直接从连接池、线程中取出资源即可进行使用,使用完后要记得交还连接池、线程池,通过池容器来对资源进行管理。

       第三部分:对于连接池维护,连接池、线程池来维护连接、线程状态,不可用连接、线程进行销毁,正在使用连接、线程进行状态标注,连接、线程不够后并且少于设置最大连接、线程数,要进行新连接、线程创建。

       通过上边可以了解到各种连接池技术以及线程池原理或者说套路,理解原理才能不被纷繁复杂表象掩盖。

       下面谈谈构建自己连接池,其实理解了连接池、线程原理,可以使用 ArrayList 来构建自己连接池、线程池。初始化时创建配置连接数、线程,存储在 ArrayList 容器中,使用时从 ArrayList 从取出连接、线程进行使用,执行完任务后,提交回 ArrayList 容器。前提条件是单线程,在多线程状态下要用线程安全容器。

       前边根据原理介绍了一个简单连接池、线程池怎样构建,实际工业级别线程池还要考虑到连接状态,短连接重连,线程池维护管理高效,线程池稳定等多个因素。

       需要用到连接池而又没有相关开源产品可用时,java 连接池可以使用 common-pool2 来构建,比如 google 开源 gRPC 技术,本身是高性能跨平台技术,但目前作为微服务使用,没有连接池、负载均衡等相应配套,这时可以根据需要自己基于 Java 容器构建自己连接池。也可以利用 common-pool2 构建连接池来提升应用性能,以及保持高可用。common-pool2 本身不仅仅可以构建连接池使用,还可以用来构建对象池。

       连接池还有一个副作用就是实现了高可用,在微服务场景下一个连接不可用,那么再从 netty 连接池中取出一个进行使用,避免了连接不可用问题。

       掌握原理从比较全面掌握各种池技术,避免数据库连接池,再到 httpclient http 连接池,再到微服务 netty 连接池,redis 客户端连接池,以及 jdk 中线程池,对象池各种各样池技术,使我们眼花缭乱,花费过多时间,掌握原理机制以不变应万变。

       推广一下这个方法,其他技术也是类似,深入掌握其原理,就可以明白其他类似技术相似原理,避免疲于应对各种新技术。但每一种架构设计与实现又与领域有着关系,也不可讲原理不顾实际情况扩展。理论与架构设计、源码学习相结合才是最好的,希望有帮助。

JDBC 之: 数据库连接池 

转自:

什么情况下使用连接池?

对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。但是对于一个复杂的数据库应用,情况就完全不同了。频繁的建立、关闭连接,会极大的减低系统的性能,因为对于连接的使用成了系统性能的瓶颈。

使用连接池的好处

  1. 连接复用。通过建立一个数据库连接池以及一套连接使用管理策略,使得一个数据库连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。
  2. 对于共享资源,有一个很著名的设计模式:资源池。该模式正是为了解决资源频繁分配、释放所造成的问题的。把该模式应用到数据库连接管理领域,就是建立一个数据库连接池,提供一套高效的连接分配、使用策略,最终目标是实现连接的高效、安全的复用。

连接池的实现

数据库连接池的基本原理是在内部对象池中维护一定数量的数据库连接,并对外暴露数据库连接获取和返回方法。

外部使用者可通过 getConnection 方法获取连接,使用完毕后再通过 close 方法将连接返回,注意此时连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。

Java 中有一个 DataSource 接口, 数据库连接池就是 DataSource 的一个实现

下面我们自己实现一个数据库连接池:

首先实现 DataSource, 这里使用 BlockingQueue 作为池 (只保留了关键代码)


import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Logger;

public class MyDataSource implements DataSource {

    static {
        try {Class.forName("com.mysql.jdbc.Driver");
        } catch (Exception e) {e.printStackTrace();
        }
    }

    // 这里有个坑
    //MySQL 用的是 5.5 的
    // 驱动用的是最新的
    // 连接的时候会报 The server time zone value '�й���׼ʱ��'
    // is unrecognized or represents more than one time zone
    // 解决方法:
    //1. 在连接串中加入?serverTimezone=UTC
    //2. 在 mysql 中设置时区,默认为 SYSTEM
    //set global time_zone='+8:00'
    private String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
    private String user = "root";
    private String password = "123456";

    private BlockingQueue<Connection> pool = new ArrayBlockingQueue<>(3);

    public MyDataSource() {initPool();
    }

    private void initPool() {
        try {for (int i = 0; i < 3; i++) {
                pool.add(new MyConnection(DriverManager.getConnection(url, user, password), this));
            }
        } catch (SQLException e) {e.printStackTrace();
        }
    }

    /*
    从池中获取连接
     */
    @Override
    public synchronized Connection getConnection() throws SQLException {
        try {return pool.take();
        } catch (InterruptedException e) {e.printStackTrace();
        }
        throw new RuntimeException("get connection failed!");
    }

    public BlockingQueue<Connection> getPool() {return pool;}

    public void setPool(BlockingQueue<Connection> pool) {this.pool = pool;}
}

实现自己的连接, 对原生连接进行封装, 调用 close 方法的时候将连接放回到池中


import java.sql.*; import java.util.Map; import java.util.Properties; importjava.util.concurrent.Executor; public class MyConnection implements Connection {// 包装的连接 private Connection conn; private MyDataSource dataSource; public MyConnection(Connection conn, MyDataSource dataSource) {this.conn = conn; this.dataSource = dataSource;} @Overridepublic Statement createStatement() throws SQLException { return conn.createStatement(); }@Override public PreparedStatement prepareStatement(String sql) throws SQLException {returnconn.prepareStatement(sql); } @Override public boolean getAutoCommit() throws SQLException {return conn.getAutoCommit(); } @Override public void setAutoCommit(boolean autoCommit) throwsSQLException {conn.setAutoCommit(autoCommit); } @Override public void commit() throwsSQLException { conn.commit(); } @Override public void rollback() throws SQLException { conn.rollback(); } @Override public void close() throws SQLException { // 解决重复关闭问题 if(!isClosed()) {dataSource.getPool().add(this); } } @Override public boolean isClosed()throws SQLException { return dataSource.getPool().contains(this); } }

main 方法


import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException;import java.sql.Statement; public class Main {public static void main(String[] args) {DataSource source = new MyDataSource(); try {Connection conn = source.getConnection(); Statement st = conn.createStatement(); st.execute("INSERT INTO USER (name,age) values('bob',12)"); conn.close();} catch (SQLException e) {e.printStackTrace(); } } }

常用数据库连接池

  • Apache DBCP

    • 官网
  • C3P0

    • 官网
  • Druid

    • GitHub

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

在 Java Web 开发过程中,会广泛使用到数据源。
我们基本的使用方式,是通过 Spring 使用类似如下的配置,来声明一个数据源:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <property name="maxActive" value="100" />
        <property name="maxIdle" value="20" />
        <property name="validationQuery" value="SELECT 1 from dual" />
        <property name="testOnBorrow" value="true" />
    </bean>

在之后应用里对于数据库的操作,都基于这个数据源,但这个数据源连接池的创建、销毁、管理,对于用户都是近乎透明的,甚至数据库连接的获取,我们都看不到 Connection 对象了。
这种方式是应用自身的数据库连接池,各个应用之间互相独立。

在类似于 Tomcat 这样的应用服务器内部,也有提供数据源的能力,这时的数据源,可以为多个应用提供服务。

这一点类似于以前写过关于 Tomcat 内部的 Connector 对于线程池的使用,可以各个 Connector 独立使用线程池,也可以共用配置的Executor。(Tomcat 的 Connector 组件)

那么,在 Tomcat 中,怎么样配置和使用数据源呢?

  1. 先将对应要使用的数据库的驱动文件 xx.jar 放到 TOMCAT_HOME/lib 目录下。
  2. 编辑 TOMCAT_HOME/conf/context.xml 文件,增加类似于下面的内容:
<Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource"
          maxTotal="100" maxIdle="30" maxWaitMillis="10000"
          username="root" password="pwd" driverClassName="com.mysql.jdbc.Driver"
          url="jdbc:mysql://localhost:3306/test"/>
  1. 需要提供数据源的应用内,使用 JNDI 的方式获取
Context initContext = new InitialContext();
Context envContext  = (Context)initContext.lookup("java:/comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/TestDB");
Connection conn = ds.getConnection();
  1. 愉快的开始使用数据库…

我们看,整个过程也并不比使用 Spring 等框架进行配置复杂,在应用内获取连接也很容易。多个应用都可以通过第 3 步的方式获取数据源,这使得同时提供多个应用共享数据源很容易。

这背后的是怎么实现的呢?

这个容器的连接池是怎么工作的呢,我们一起来看一看。

在根据 context.xml 中配置的 Resouce 初始化的时候,会调用具体 DataSource 对应的实现类,Tomcat 内部默认使用的 BasicDataSource,在类初始化的时候,会执行这样一行代码 DriverManager.getDrivers(),其对应的内容如下,主要作用是使用 java.sql.DriverManager 实现的 Service Provider 机制,所有 jar 文件包含 META-INF/services/java.sql.Driver 文件的,会被自动发现、加载和注册,不需要在需要获取连接的时候,再手动的加载和注册。

public static java.util.Enumeration<Driver> getDrivers() {java.util.Vector<Driver> result = new java.util.Vector<>();
        for(DriverInfo aDriver : registeredDrivers) {if(isDriverAllowed(aDriver.driver, callerClass)) {result.addElement(aDriver.driver);
            } else {println("skipping:" + aDriver.getClass().getName());
            }
        }
        return (result.elements());
    }

之后 DataSourceFactory 会读取 Resouce 中指定的数据源的属性,创建数据源。

在我们的应用内 getConnection 的时候,使用 ConnectionFactory 创建 Connection, 注意在创建 Connection 的时候,重点代码是这个样子:

public PooledObject<PoolableConnection> makeObject() throws Exception {Connection conn = _connFactory.createConnection();
        initializeConnection(conn);
        PoolableConnection pc = new PoolableConnection(conn,_pool, connJmxName);
        return new DefaultPooledObject<>(pc);

这里的 _pool 是 GenericObjectPool,连接的获取是通过其进行的。

public Connection getConnection() throws SQLException {C conn = _pool.borrowObject();
}

在整个 pool 中包含几个队列,其中比较关键的一个定义如下:

private final LinkedBlockingDeque<PooledObject<T>> idleObjects;

我们再看连接的关闭,

public void close() throws SQLException {if (getDelegateInternal() != null) {super.close();
        super.setDelegate(null);
    }
}

这里的关闭,并不会真的调用到 Connection 的 close 方法,我们通过上面的代码已经看到,Connection 返回的时候,其实是 Connection 的 Wrapper 类。在 close 的时候,真实的会调用到下面的代码

// Normal close: underlying connection is still open, so we
           // simply need to return this proxy to the pool
           try {_pool.returnObject(this);
           } catch(IllegalStateException e) {}

所谓的return, 是把连接放回到上面我们提到的 idleObjects 队列中。整个连接是放在一个 LIFO 的队列中,所以如果没有关闭或者超过最大空闲连接,就会加到队列中。而允许外的连接才会真实的销毁destory

int maxIdleSave = getMaxIdle();
        if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
            try {destroy(p);
            } catch (Exception e) {swallowException(e);
            }
        } else {if (getLifo()) {idleObjects.addFirst(p); // 这里。} else {idleObjects.addLast(p);
            }
            if (isClosed()) {
                // Pool closed while object was being added to idle objects.
                // Make sure the returned object is destroyed rather than left                // in the idle object pool (which would effectively be a leak)
                clear();}
        }

总结下:以上是 Tomcat 内部实现的 DataSource 部分关键代码。数据源我们有时候也称为连接池,所谓池的概念,就是一组可以不断重用的资源,在使用完毕后,重新恢复状态,以备再次使用。
而为了达到重用的效果,对于客户端的关闭操作,就不能做真实意义上的物理关闭,而是根据池的状态,以执行具体的入队重用,还是执行物理关闭。无论连接池,还是线程池,池的原理大致都是这样的。

正文完
 0