7.java程序员必知必会类库之数据库连接池
前言
在java中,“池”化的设计思想随处可见,池化的最终目的是为了对象复用,降低系统创建、销毁对象的成本,提升资源的可管理性。
尤其是一些大对象,创建销毁比较消耗资源的对象,池化可以极大提高效率,减少系统响应时间,提高系统并发度。
常见的有线程池,实例池(spring容器),连接池等。本节我们介绍连接池里面的数据库连接池。
1. 有无连接池管理图示
1.1 没有连接池管理的时候
没有数据库连接池管理的话,每次外部请求过来,请求到我们web层,dao层为每个请求都会创建连接,执行脚本,释放连接,其中dao层和数据库建立连接,底层都是TCP请求,每次都要三次握手,四次挥手。如下如所示:
1.2 交给连接池管理
在交给连接池管理连接后,dao层和数据库交互的时候,直接从连接池获取连接。不再自己手动创建销毁连接,如下图所示:
1.3 手动创建连接代码demo
1.3.1 pom坐标引入
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.15</version><scope>runtime</scope>
</dependency>
1.3.2 测试demo
@Test
public void test() throws Exception{StopWatch stopWatch=new StopWatch();stopWatch.start();for (int i = 0; i < 10000; i++) {//步骤//1 获取数据库连接的URL mysql8 必须要给一个时区 serverTimezone=UTCString url="jdbc:mysql://xx.xx.xx.xx:3306/openplatform?serverTimezone=UTC";//2 获取数据库连接的用户名和密码String username="xxxlatformopr";String password="xxx";//3 加载驱动Class.forName("com.mysql.cj.jdbc.Driver");//4 获取数据库连接对象 DriverManager依据数据库的不同,管理JDBC驱动Connection connection= DriverManager.getConnection(url,username,password);//5 获取操作数据库的Statement对象Statement statement=connection.createStatement();//6向数据库发送sqlString sql="select * from t_cust_info limit 1";//7 通过statement对象 发送查询请求 拿回结果集ResultSet resultSet = statement.executeQuery(sql);//8遍历结果集 做一系列操作while (resultSet.next()){Object id = resultSet.getObject("cust_id");//System.out.println(id);Object uname = resultSet.getObject("cust_name");//System.out.println(uname);Object idNo = resultSet.getObject("id_no");//System.out.println(idNo);// System.out.println("==============");}//关闭资源resultSet.close();statement.close();connection.close();}stopWatch.stop();double totalTimeSeconds = stopWatch.getTotalTimeSeconds();System.out.println(totalTimeSeconds);//5.644
}
1.4 性能分析
在上面的代码中,循环遍历了10000次,每次都会重新创建连接,查询,销毁链接。本地测试100次,平均每次耗时44.09秒,中间甚至偶发的报错,tcp连接超过限制
2. C3p0
2.1 介绍
C3PO是一个开源的JDBC连接池,它能够自动维护和回收数据库连接使用C3PO,可以设置最大连接数、最小连接数、最大空闲时间等参数从而对连接进行有效控制。同时,C3PO还提供了丰富的监控功能,可以帮助我们追踪和排除数据库连接问题
2.2 使用
2.2.1 pom坐标引入
<dependency><groupId>com.mchange</groupId><artifactId>c3p0</artifactId><version>0.9.5.2</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.15</version><scope>runtime</scope>
</dependency>
2.2.2 代码demo
2.2.2.1 连接工具类
public class C3p0Util {private static DataSource dataSource = null;static{//与配置文件的配置名字 named-config需要保持一致dataSource = new ComboPooledDataSource("mysqlapp");}//从连接池中获取连接public static Connection getConnection(){try {return dataSource.getConnection();} catch (SQLException e) {e.printStackTrace(); }return null;}//释放连接回连接池public static void release(Connection conn, Statement stmt, ResultSet rs) {if (rs != null) {try {rs.close();} catch (Exception e) {e.printStackTrace();}rs = null;}if (stmt != null) {try {stmt.close();} catch (Exception e) {e.printStackTrace();}stmt = null;}if (conn != null) {try {conn.close();} catch (Exception e) {e.printStackTrace();}conn = null;}}
}
2.2.2.2 c3p0-config.xml模板
放到项目resource下面
<c3p0-config><named-config name="mysqlapp"><property name="driverClass">com.mysql.cj.jdbc.Driver</property><property name="jdbcUrl">jdbc:mysql://x.x.x.x:3306/openplatform</property><property name="user">formopr</property><property name="password">xxxxx</property><!-- 进行数据库连接池管理的基本信息 --><!-- 当数据库连接池中的连接数不够时,c3p0一次性向数据库服务器申请的连接数 --><property name="acquireIncrement">5</property><!-- c3p0数据库连接池中初始化时的连接数 --><property name="initialPoolSize">10</property><!-- c3p0数据库连接池维护的最少连接数 --><property name="minPoolSize">10</property><!-- c3p0数据库连接池维护的最多的连接数 --><property name="maxPoolSize">200</property><!-- c3p0数据库连接池最多维护的Statement的个数 --><property name="maxStatements">50</property><!-- 每个连接中可以最多使用的Statement的个数 --><property name="maxStatementsPerConnection">2</property></named-config>
</c3p0-config>
2.2.2.3 测试demo
@Test
public void testC3p0() throws Exception{StopWatch stopWatch=new StopWatch();stopWatch.start();for (int i = 0; i < 10000; i++) {//步骤//4 获取数据库连接对象 DriverManager依据数据库的不同,管理JDBC驱动Connection connection = C3p0Util.getConnection();//5 获取操作数据库的Statement对象Statement statement=connection.createStatement();//6向数据库发送sqlString sql="select * from t_cust_info limit 1";//7 通过statement对象 发送查询请求 拿回结果集ResultSet resultSet = statement.executeQuery(sql);//8遍历结果集 做一系列操作while (resultSet.next()){Object id = resultSet.getObject("cust_id");//System.out.println(id);Object uname = resultSet.getObject("cust_name");//System.out.println(uname);Object idNo = resultSet.getObject("id_no");//System.out.println(idNo);// System.out.println("==============");}C3p0Util.release(connection,statement,resultSet);}stopWatch.stop();double totalTimeSeconds = stopWatch.getTotalTimeSeconds();System.out.println(totalTimeSeconds);
}
2.3 性能测试
上面的代码,本地测试100次,平均每次运行耗时:5.671s,可以看到相对自己手动创建链接,时间大幅减少
2.4 常用参数配置
<c3p0-config> <default-config> <!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 --> <property name="acquireIncrement">3</property> <!--定义在从数据库获取新连接失败后重复尝试的次数。Default: 30 --> <property name="acquireRetryAttempts">30</property> <!--两次连接中间隔时间,单位毫秒。Default: 1000 --> <property name="acquireRetryDelay">1000</property> <!--连接关闭时默认将所有未提交的操作回滚。Default: false --> <property name="autoCommitOnClose">false</property> <!--c3p0将建一张名为Test的空表,并使用其自带的查询语句进行测试。如果定义了这个参数那么 属性preferredTestQuery将被忽略。你不能在这张Test表上进行任何操作,它将只供c3p0测试 使用。Default: null--> <property name="automaticTestTable">Test</property> <!--获取连接失败将会引起所有等待连接池来获取连接的线程抛出异常。但是数据源仍有效 保留,并在下次调用getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试 获取连接失败后该数据源将申明已断开并永久关闭。Default: false--> <property name="breakAfterAcquireFailure">false</property> <!--当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出 SQLException,如设为0则无限期等待。单位毫秒。Default: 0 --> <property name="checkoutTimeout">100</property> <!--通过实现ConnectionTester或QueryConnectionTester的类来测试连接。类名需制定全路径。 Default: com.mchange.v2.c3p0.impl.DefaultConnectionTester--> <property name="connectionTesterClassName"></property> <!--指定c3p0 libraries的路径,如果(通常都是这样)在本地即可获得那么无需设置,默认null即可 Default: null--> <property name="factoryClassLocation">null</property> <!--强烈不建议使用该方法,将这个设置为true可能会导致一些微妙而奇怪的bug--> <property name="forceIgnoreUnresolvedTransactions">false</property> <!--每60秒检查所有连接池中的空闲连接。Default: 0 --> <property name="idleConnectionTestPeriod">60</property> <!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 --> <property name="initialPoolSize">3</property> <!--最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 --> <property name="maxIdleTime">60</property> <!--连接池中保留的最大连接数。Default: 15 --> <property name="maxPoolSize">15</property> <!--JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0--> <property name="maxStatements">100</property> <!--maxStatementsPerConnection定义了连接池内单个连接所拥有的最大缓存statements数。Default: 0 --> <property name="maxStatementsPerConnection"></property> <!--c3p0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能 通过多线程实现多个操作同时被执行。Default: 3--> <property name="numHelperThreads">3</property> <!--当用户调用getConnection()时使root用户成为去获取连接的用户。主要用于连接池连接非c3p0 的数据源时。Default: null--> <property name="overrideDefaultUser">root</property> <!--与overrideDefaultUser参数对应使用的一个参数。Default: null--> <property name="overrideDefaultPassword">password</property> <!--密码。Default: null--> <property name="password"></property> <!--定义所有连接测试都执行的测试语句。在使用连接测试的情况下这个一显著提高测试速度。注意: 测试的表必须在初始数据源的时候就存在。Default: null--> <property name="preferredTestQuery">select id from test where id=1</property> <!--用户修改系统配置参数执行前最多等待300秒。Default: 300 --> <property name="propertyCycle">300</property> <!--因性能消耗大请只在需要的时候使用它。如果设为true那么在每个connection提交的 时候都将校验其有效性。建议使用idleConnectionTestPeriod或automaticTestTable 等方法来提升连接测试的性能。Default: false --> <property name="testConnectionOnCheckout">false</property> <!--如果设为true那么在取得连接的同时将校验连接的有效性。Default: false --> <property name="testConnectionOnCheckin">true</property> <!--用户名。Default: null--> <property name="user">root</property> <!--早期的c3p0版本对JDBC接口采用动态反射代理。在早期版本用途广泛的情况下这个参数 允许用户恢复到动态反射代理以解决不稳定的故障。最新的非反射代理更快并且已经开始 广泛的被使用,所以这个参数未必有用。现在原先的动态反射与新的非反射代理同时受到 支持,但今后可能的版本可能不支持动态反射代理。Default: false--> <property name="usesTraditionalReflectiveProxies">false</property> </default-config>
</c3p0-config>
3 DBCP
3.1 介绍
DBCP是Apache软件基金会下的一个开源项目,也是一个JDBC连接池。与 C3PO类似,DBCP 也能够自动维护和回收数据库连接。同时,DBCP还支持连接池配置文件的读取,这样就可以通过修改配置文件来改变连接池的参数
3.2 使用
3.2.1 pom坐标引入
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.15</version><scope>runtime</scope>
</dependency>
<dependency><groupId>commons-dbcp</groupId><artifactId>commons-dbcp</artifactId><version>1.4</version>
</dependency>
<dependency><groupId>commons-pool</groupId><artifactId>commons-pool</artifactId><version>1.6</version>
</dependency>
3.2.2 代码demo
3.2.2.1 连接工具类
public class JDbcConfigReadUtil {private static DataSource dataSource = null;static {Properties props = new Properties();try {File file = new File("E:\\\\weixinData\\\\WeChat Files\\\\wxid_gv8xbkloz0wc22\\\\FileStorage\\\\File\\\\2023-03\\\\test\\\\src\\\\main\\\\resources\\\\dbcp\\\\jdbc.properties");//获取文件InputStream inputStream = new FileInputStream(file);//获取输入流props.load(inputStream);//props需要通过输入流读取一个.Properties文件dataSource = BasicDataSourceFactory.createDataSource(props);} catch (Exception e) {e.printStackTrace();}}//从连接池中获取连接public static Connection getConnection() {try {return dataSource.getConnection();} catch (SQLException e) {e.printStackTrace();}return null;}//释放连接回连接池public static void release(Connection conn, Statement stmt, ResultSet rs) {if (rs != null) {try {rs.close();} catch (Exception e) {e.printStackTrace();}rs = null;}if (stmt != null) {try {stmt.close();} catch (Exception e) {e.printStackTrace();}stmt = null;}if (conn != null) {try {conn.close();} catch (Exception e) {e.printStackTrace();}conn = null;}}
}
3.2.2.2 配置properties文件
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://x.x.x.x:3306/openplatform
username=xx
password=xxx
3.2.2.3 测试demo
@Test
public void testDbcp() throws Exception{StopWatch stopWatch=new StopWatch();stopWatch.start();for (int i = 0; i < 10000; i++) {//4 获取数据库连接对象 DriverManager依据数据库的不同,管理JDBC驱动Connection connection = JDbcConfigReadUtil.getConnection();//5 获取操作数据库的Statement对象Statement statement=connection.createStatement();//6向数据库发送sqlString sql="select * from t_cust_info limit 1";//7 通过statement对象 发送查询请求 拿回结果集ResultSet resultSet = statement.executeQuery(sql);//8遍历结果集 做一系列操作while (resultSet.next()){Object id = resultSet.getObject("cust_id");//System.out.println(id);Object uname = resultSet.getObject("cust_name");//System.out.println(uname);Object idNo = resultSet.getObject("id_no");//System.out.println(idNo);// System.out.println("==============");}C3p0Util.release(connection,statement,resultSet);}stopWatch.stop();double totalTimeSeconds = stopWatch.getTotalTimeSeconds();System.out.println(totalTimeSeconds);}
3.3 性能测试
上面的代码,本地测试100次,平均每次运行耗时:5.191,可以看到相对自己手动创建链接,时间大幅减少
3.4 常用参数配置
<!--初始化连接:连接池启动时创建的初始化连接数量-->
initialSize=5
<!--maxActive: 最大连接数量-->
maxActive=30
<!-- 连接在池中保持空闲而不被空闲连接回收器线程-->
minEvictableIdleTimeMillis=1800000
<!--maxIdle: 最大空闲连接-->
maxIdle=5
<!--minIdle: 最小空闲连接-->
minIdle=2
<!--maxWait: 超时等待时间以毫秒为单位 1000等于60秒-->
maxWait=1000
<!--对于事务是否 autoCommit-->
defaultAutoCommit=true
<!-- 在空闲连接回收器线程运行期间休眠的时间值,以毫秒为单位. -->
timeBetweenEvictionRunsMillis=600000<!-- 在每次空闲连接回收器线程(如果有)运行时检查的连接数量 -->
numTestsPerEvictionRun=3
<!--是否自动回收超时连接-->
removeAbandoned=true
<!--超时时间(以秒数为单位)-->
removeAbandonedTimeout=180
<!-- 连接被泄露时是否打印 -->
logAbandoned=true
<!--连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.设置为true后如果要生效,validationQuery参数必须设置为非空字符串-->
testWhileIdle=true
validationQuery=select 1
<!--是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个.-->
testOnBorrow=true
<!--是否在归还到池中前进行检验-->
testOnReturn=false
4 Druid
4.1 介绍
Druid 是阿里巴巴开源的一个数据库连接池。Druid 具有数据源监控SQL监控、容器集成支持、数据源防火墙等特性,可以帮助我们更好地管理数据库连接。同时,Druid 还提供了很多性能优化功能,如预编译语句缓存、分布式ID生成器等
4.2 使用
4.2.1 pom坐标引入
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.16</version>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.15</version>
</dependency>
4.2.2 代码demo
4.2.2.1 连接工具类
public class DruidUtil {private static DataSource dataSource = null;static {Properties props = new Properties();try {File file = new File("E:\\\\weixinData\\\\WeChat Files\\\\wxid_gv8xbkloz0wc22\\\\FileStorage\\\\File\\\\2023-03\\\\test\\\\src\\\\main\\\\resources\\\\druid\\\\druid.properties");//获取文件InputStream inputStream = new FileInputStream(file);//获取输入流props.load(inputStream);//props需要通过输入流读取一个.Properties文件dataSource=DruidDataSourceFactory.createDataSource(props);} catch (Exception e) {e.printStackTrace();}}//从连接池中获取连接public static Connection getConnection() {try {return dataSource.getConnection();} catch (SQLException e) {e.printStackTrace();}return null;}//释放连接回连接池public static void release(Connection conn, Statement stmt, ResultSet rs) {if (rs != null) {try {rs.close();} catch (Exception e) {e.printStackTrace();}rs = null;}if (stmt != null) {try {stmt.close();} catch (Exception e) {e.printStackTrace();}stmt = null;}if (conn != null) {try {conn.close();} catch (Exception e) {e.printStackTrace();}conn = null;}}
}
4.2.2.2 配置文件
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://x.x.x.x:3306/openplatform
username=xx
password=xxx
4.2.2.3 测试demo
@Test
public void testDruid() throws Exception{StopWatch stopWatch=new StopWatch();stopWatch.start();for (int i = 0; i < 10000; i++) {//4 获取数据库连接对象 DriverManager依据数据库的不同,管理JDBC驱动Connection connection = DruidUtil.getConnection();//5 获取操作数据库的Statement对象Statement statement=connection.createStatement();//6向数据库发送sqlString sql="select * from t_cust_info limit 1";//7 通过statement对象 发送查询请求 拿回结果集ResultSet resultSet = statement.executeQuery(sql);//8遍历结果集 做一系列操作while (resultSet.next()){Object id = resultSet.getObject("cust_id");//System.out.println(id);Object uname = resultSet.getObject("cust_name");//System.out.println(uname);Object idNo = resultSet.getObject("id_no");//System.out.println(idNo);// System.out.println("==============");}C3p0Util.release(connection,statement,resultSet);}stopWatch.stop();double totalTimeSeconds = stopWatch.getTotalTimeSeconds();System.out.println(totalTimeSeconds);}
4.3 性能测试
上面的代码,本地测试100次,平均每次运行耗时:5.58s,可以看到相对自己手动创建链接,时间大幅减少
4.4 常用参数配置
druid github 属性介绍
5. 注意事项:
- 上面演示代码连接池都是项目启动的时候,自己在工具类维护的,真实生产环境中,一般都是交给spring管理的
- 上面几个测试案例的时间响应,测试场景不完善,只是能直观上比对出来,使用连接池会比手动连接效率高很多,但是几个连接池的响应时间比较,不具有参考意义
- 关于数据库连接池配置参数多少合理,这是老生常谈的问题,与具体环境使用相关性很大,客户的并发量等都强相关,没有一套参数放到四海皆准的道理
- 如果点到具体的配置类源码里,可以看到其实jar包做的都是从配置文件读固定属性,来组装一个连接池配置类,这个连接池配置类有很多默认属性。
- 所以如果哪个属性失效了,需要第一时间看是不是配置的参数没有生效。举一反三,也可以想到,连接池的配置方式可以有xml格式,propertis格式等各种自定义格式,只要保证配置文件正常读取,读取的参数值正常封装就行
- 如果和springboot结合,可以看看有没有对应的自动装配类,将连接池的参数配置交给springboot自己装配
- 关于几个连接池,公司应该选择哪一个连接池?主要要看具体的业务场景和性能要求。如果需要丰富的管理和监控功能,可以选择 Druid 这样的连接池。如果需要简单易用,可以选择C3PO或DBCP 这样的常规连接池。笔者公司选择的是Druid连接池。
参考文献:
C3p0使用详解