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

转载自 微信公众号【码农翻身】php

网络访问html

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

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

外部的花花世界一下所有打开,  不少程序开始离开这个拥挤的城镇, 住到更加宜居的地方去。mysql

但是他们的工做仍是要读写数据库, 你们都在想办法能不能经过网络来访问数据库镇的数据库。linux

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

Java 说: “Mysql 先生, 如今已经网络时代了, 您也得与时俱进啊, 给咱们开放下网络接口吧。 ”程序员

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

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(); } } }

经常使用数据库链接池

数据源链接池的原理及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部分关键代码。数据源咱们有时候也称为链接池,所谓池的概念,就是一组能够不断重用的资源,在使用完毕后,从新恢复状态,以备再次使用。
而为了达到重用的效果,对于客户端的关闭操做,就不能作真实意义上的物理关闭,而是根据池的状态,以执行具体的入队重用,仍是执行物理关闭。不管链接池,仍是线程池,池的原理大体都是这样的。

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

                     wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

相关文章
相关标签/搜索