前言
在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=UTC
String 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向数据库发送sql
String 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向数据库发送sql
String 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向数据库发送sql
String 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向数据库发送sql
String 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使用详解