国货之光Druid连接池技术使用
连接池性能消耗问题
连接可以复用
连接池分为三个阶段
- 创建(T1)
- 使用(T2)
- 销毁(T3)
如果T1+T3 > T2不合理
连接池:节约了创建和销毁连接的性能消耗,提升了时间的响应,只有t2时间。只需从连接池中获取连接,然后再将用过的连接放回连接池中。
数据库连接池作用
不使用数据库连接池,每次都通过DriverManager获取新连接,用完直接抛弃,连接的利用率太低,太浪费。
对于数据库服务器来说,压力太大了,我们数据库服务器和java程序对连接数也无法控制,很容易导致数据库服务器崩溃。
因此,我们希望能管理连接。
1)我们可以建立一个连接池,这个池可以容纳一定数量的连接对象,一开始,我们可以先替用户先创建好一些连接对象,等用户要拿连接对象时,就直接从池中拿,不用新建,可以节省时间,然后用户用完后,放回去,别人可以接着用。
2)可以提高连接的使用率,当池中的现有连接都用完了,那么连接池可以向服务器申请新的连接放到池中。
3)直到池中的连接达到“最大连接数”,就不能在申请新的连接了,如果没有拿到连接的用户只能等待
市面常见连接池产品和对比
JDBC的数据库连接池使用javax.sql.DataSource接口进行规范,所有第三连接池都实现此接口,即所有连接池获取连接和回收连接方法都一样,不同的只有性能和扩展功能。
- DBCP 速度快,但自身存在bug
- C3P0 速度慢,但稳定性可以
- Proxool 稳定性差,有监控连接池状态功能
- Druid(国货之光,集DBCP、C3P0、Proxool优点于一身)
Druid连接池的使用
导入Druid工具类jar包
硬编码方式
直接使用代码设置连接池参数方式:
- 创建一个Druid对象
- 设置连接池参数(必须|非必须)
- 获取连接(通用方法)
- 回收连接(通用方法)
@Test
public void testHard() throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
//必须 连接数据库驱动类的全限定符
dataSource.setUrl("jdbc:mysql://localhost:3306/book");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
//非必须
dataSource.setInitialSize(5);//初始化连接数量
dataSource.setMaxActive(10);//最大的数量
//获取连接
DruidPooledConnection connection = dataSource.getConnection();
//数据库curd
//连接池提供的连接,close就是回收连接
connection.close();
}
软编码方式
编写druid.properties
druid连接池需要的配置参数,key固定命名。
通过读取外部配置文件的方法,实例化druid连接池对象
- 读取外部配置文件Properties
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=123456
url=jdbc:mysql://localhost:3306/book
- 使用连接池的工具类的工程模式,创建连接池
@Test
public void testSoft() throws Exception {
Properties properties = new Properties();
//src下的文件,可以使用类加载器提供的方法
InputStream ips = DruidUsePart.class.getClassLoader().getResourceAsStream("main/resources/druid.properties");
properties.load(ips);
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
Connection connection = dataSource.getConnection();
//数据库curd
connection.close();
}
JDBC工具类封装
实现单例模式的关键点是将类的构造函数声明为私有,这样就无法从外部直接实例化该类的对象。然后,通过在类内部定义一个静态方法或属性,来创建或获取该类的唯一实例。这样,无论在何处调用该方法或属性,都可以获得同一个实例对象。
//工具类封装1.0
package com.atguigu.api.utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* 内部包含一个连接池对象,并且对外提供获取连接和回收连接的方法
*
* 建议:工具类的方法推荐写成静态的,外部调用会更加方便
* 实现:
* 属性 : 连接池对象(实例化一次)
* 单例模式
* 静态代码块
* 方法:对外提供连接的方法
* 回收外部传入连接方法
*/
public class JdbcUtils {
private static DataSource dataSource = null;
static {
Properties properties = new Properties();
InputStream ips = JdbcUtils.class.getClassLoader().getResourceAsStream("main/resources/druid.properties");
try {
properties.load(ips);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void freeConnection(Connection connection) throws SQLException {
connection.close();
}
}
以上工具类每次用JdbcUtils.getConnection( )获取的连接是一个新的连接,不是同一个。如果不能保证是同一个连接对象,就无法保证事务的管理。
ThreadLocal 可以为同一个线程存储共享变量。为解决多线程程序的并发问题提供了一个新的思路,通常用来在多线程中管理共享数据连接、session等。
ThreadLocal用于保存某个线程共享变量,原因是在java中,每一个线程对象中都有一个ThreadLocalMap<ThreadLocal,Object>,Object即为该线程的共享变量,而这个map是通过ThreadLocal的set和get方法操作的。
//工具类封装2.0
package com.atguigu.api.utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* 利用线程本地变量,存储连接信息,确保一个线程的多个方法可以获取同一个connection
* 优势:事务操作的时候,service和dao属于同一个线程,不用在传递参数了
* 可以调用getConnection自动获取的是相同的连接
*/
public class JdbcUtilsV2 {
private static DataSource dataSource = null;
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
static {
Properties properties = new Properties();
InputStream ips = JdbcUtilsV2.class.getClassLoader().getResourceAsStream("main/resources/druid.properties");
try {
properties.load(ips);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Connection getConnection() throws SQLException {
Connection connection = tl.get();
if (connection == null){
connection = dataSource.getConnection();
tl.set(connection);
}
return connection;
}
public static void freeConnection() throws SQLException {
Connection connection = tl.get();
if (connection != null){
tl.remove();//清空线程本地变量数据
connection.setAutoCommit(true);//事务状态回归,关闭事务
connection.close();
}
}
}
JDBC的步骤
- 注册驱动
- 获取连接
- 编写SQL语句
- 创建statement对象
- 占位符赋值
- 发送SQL语句
- 结果集解析
- 回收资源
工具类封装做的事情只满足注册驱动、获取连接和回收资源
高级应用层封装BaseDao
基本上每一个数据表都应该有一个对应的dao接口及其实现类,发现所有表的操作(CURD)代码重复度很高,所有可以抽取公共代码,给这些DAO的实现类可以抽取一个公共的父类,我们称之为BaseDao。
在BaseDao封装方法只用封装两个,而不是四个,因为查询是一类,增删改是一类,它们都是返回int型,传入SQL语句。
package com.atguigu.api.utils;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/** TODO:封装dao数据库重复代码
* 封装两个方法:一个简化非DQL,一个简化DQL
*/
public abstract class BaseDao {
//可变参数需存在形参列表的最后一位
/**
* @param sql 带占位符的SQL语句
* @param params 占位符的值
* @return 返回执行影响的行数
*/
public int executeUpdate(String sql ,Object ... params) throws SQLException {
Connection connection = JdbcUtilsV2.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//占位符赋值,可变参数可以当做数组使用,占位符赋值是从1开始,但是数组的值是从0开始的
for (int i = 1; i <= params.length; i++) {
//占位符的值必须等于sql语句?的位置
preparedStatement.setObject(i, params[i-1]);
}
int row = preparedStatement.executeUpdate();
preparedStatement.close();
//connection.close();不合适,如果是事务操作不关闭
//connection.setAutoCommit(false); 开启事务,业务层处理会回收,不要管连接
if(connection.getAutoCommit()) {
//没有开启事务,正常回收连接
JdbcUtilsV2.freeConnection();
} return row;
}
/**
* 非DQL语句封装方法 返回值固定为int
*
* DQL语句封装方法 返回类型 List<T>某一实体类的集合
* 并不是List<Map> map中的key和value虽然可以自定义,
* 但是map没有数据校验机制(即年龄可以为负数),也不支持反射操作
* 数据库数据 --> java实体类
* 表中的一行数据对应java类的一个对象,表对应一个类,多行数据对应List<java实体类> list;
* 实体类是有校验机制的,用get和set方法做参数校验,实体类也支持反射操作
* @param Class<T> clazz 有两点好处:1.确定泛型 例:传入User.class T = User 2.要使用反射机制给属性赋值
*/
/**
* 将查询结果封装到一个实体类集合
* @param clazz 反射实例化对象,同时给泛型赋值,T代表着Class泛型对应的值
* @param sql 查询语句,要求列名或者别名等于实体类的属性名
* @param params 占位符的值要和?位置对象对应
* @return List<T>返回的集合是T对应的类型,返回查询的实体类集合
* @param <T> <T> 声明一个泛型是T,不确定类型
*/
public <T> List<T> executeQuery(Class<T> clazz, String sql,Object... params) throws SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
Connection connection = JdbcUtilsV2.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
if(params != null && params.length != 0){
for (int i = 1; i <= params.length ; i++) {
preparedStatement.setObject(i, params[i-1]);
}
}
//发送SQL语句
ResultSet resultSet = preparedStatement.executeQuery();
//结果集解析
List<T> list = new ArrayList<T>();
//metaData装的当前列的信息对象
ResultSetMetaData metaData = resultSet.getMetaData();
//可以水平遍历
int columnCount = metaData.getColumnCount();
while(resultSet.next()){
//一行数据对应一个T类型对象
T t = clazz.newInstance();//调用类的无参构造函数实例化对象
// 自动遍历列
for (int i = 1; i <= columnCount; i++) {
//对象属性值
Object value = resultSet.getObject(i);
//获取指定列下角标的列的名称
String propertyName = metaData.getColumnLabel(i);
//反射,给对象的属性值赋值
Field field = clazz.getDeclaredField(propertyName);
field.setAccessible(true);//属性可以设置,打破private的修饰限制
//参数1要赋值的对象,如果属性是静态,第一个参数可以为null
//参数2 具体的属性值
field.set(t,value);
}
list.add(t);
}
resultSet.close();
preparedStatement.close();
if(connection.getAutoCommit()){
//没有事务,可以回收连接
JdbcUtilsV2.freeConnection();
}
return list;
}
}