数据库访问
访问数据库主要有以下几个步骤:
- 加载数据库驱动
- 创建数据库连接
- 执行访问操作并处理执行结果
- 关闭连接,释放资源
在每一次请求数据库都要经历上述过程,创建连接和释放资源也都是些重复性的动作,当请求量比较大时,资源是个很大的浪费。如何优化呢,可以使用连接池。
连接池
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。
原理分析
数据库连接是访问数据库必须的,可以在系统初始化时提前创建一定数量的连接,保存起来,当有创建连接的请求过来时,就直接拿出来,标记为使用中(避免与其他请求拿到同一个),使用完后,再放回连接池中。过程如下
- 系统在启动时初始化连接池;
- 向连接池请求可用的数据库连接;
- 如果没有获取到可用的数据库连接,并且连接池中连接的数量小于最大连接数,则按照规定的步长给连接池中添加连接,然后再获取,如果连接池中的数量已经到了最大连接数还没有获取到可用的连接,则等待其他请求释放了连接后再获取;
- 使用获取到的数据库连接请求数据库;
- 将数据库连接放回连接池,供其他连接使用;
简单模拟实现
/**
* 连接池对象
*/
public class Pool {
private String driver = null;//数据库驱动
private String url = null;//连接地址
private String username = null;//用户
private String password = null;//密码
//初始化连接数
private static int initSize = 2;
//池中最大连接数
private static int maxSize = 5;
//每次创建的连接数
private static int stepSize = 2;
//超时时间
private static int timeout = 2000;
//用来保存创建的数据库连接
private List<PooledConnection> connectionPool = new ArrayList<PooledConnection>();
private Lock lock = new ReentrantLock();
public Pool(String driver, String url, String username, String password) throws Exception {
this.driver = driver;
this.url = url;
this.username = username;
this.password = password;
//创建连接池时初始化initSize个数据库连接放入池中
resizePool(initSize);
}
/**
* 初始化连接池
* @param num 初始时按照initSize给池中添加连接,其他时候按照stepSize给池中加
*/
private void resizePool(int num) throws Exception {
//池中现有的连接数
int currentNum = connectionPool.size();
//池中的连接数不能超过设置的最大连接数
if (maxSize < currentNum + num) {
num = maxSize - currentNum;
}
//创建连接放入池中
for(int i=0; i<num; i++){
PooledConnection conn = newPooledConnection();
connectionPool.add(conn);
}
}
/**
* 创建连接池中的连接对象,包含了状态(忙、闲)
* @return
*/
private PooledConnection newPooledConnection() throws Exception {
Connection conn = createConnection();//数据库连接
PooledConnection pconn = new PooledConnection(conn);//连接池中的连接
return pconn;
}
/**
* 创建数据库连接
* @return
* @throws SQLException
*/
private Connection createConnection() throws Exception {
//加载驱动
this.getClass().getClassLoader().loadClass(driver);
//创建连接
Connection conn = null;
conn = DriverManager.getConnection(url, username, password);
return conn;
}
/**
* 获取数据库连接
* @return
*/
public synchronized Connection getConnection() throws Exception {
Connection conn = null;
//从连接池中获取连接
if(connectionPool.size() > 0){
//获取一个空闲的数据库连接
conn = getFreeConnFromPool();
//没有获取到连接
while(conn == null){
//隔2秒 重新获取
System.out.println(Thread.currentThread().getName() + " 等待获取连接");
Thread.sleep(2000);
conn = getFreeConnFromPool();
}
}
return conn;
}
/**
* 从连接池中获取空闲的连接
* @return
*/
private Connection getFreeConnFromPool() throws Exception {
Connection conn = null;
//获取可用的连接
conn = findAvailableConn();
//没有获取到可用的连接
if(conn == null){
//重新添加数据库连接到连接池中
resizePool(stepSize);
//获取可用的连接
conn = findAvailableConn();
}
return conn;
}
/**
* 获取一个可用的连接
* @return
* @throws Exception
*/
private Connection findAvailableConn() throws Exception {
Connection conn = null;
if(connectionPool.size() > 0){
for(PooledConnection cip : connectionPool){
if(!cip.isBusy()){
conn = cip.getConn();
cip.setBusy(true);//获取后将当前连接状态标记为 执行
//判断当前连接是否可用
if(!conn.isValid(timeout)){
//conn.isValid如果连接未关闭且有效,则返回true
//当前连接池连接的数据库连接有问题,创建一个新的数据库连接代替它
conn = createConnection();
cip.setConn(conn);
}
break;
}
}
}
return conn;
}
/**
* 把连接返回连接池
* 把连接返回给连接池就是把状态标记为‘闲’,可以让其他请求使用
*/
public void returnConnToPool(Connection conn){
for (PooledConnection cip : connectionPool) {
if (cip.getConn() == conn) {
cip.setBusy(false);//设置为空闲
System.out.println(Thread.currentThread().getName() + " 释放了连接");
break;
}
}
}
}
/**
* 连接池中的连接对象
*/
public class PooledConnection {
//数据库连接
private Connection conn;
//用于标识当前数据库连接的状态 true:执行 false:空闲
private boolean busy;
public PooledConnection(Connection conn) {
this.conn = conn;
}
// 此处省略get set方法
}
测试
public class App {
public static void main(String[] args) throws Exception {
//创建一个连接池
Pool pool = new Pool("com.mysql.jdbc.Driver", "jdbc:mysql://localhost:3306/test",
"root", "123456");
//创建7个线程,模拟并发
Thread[] threads = new Thread[7];
for(int i=0;i<threads.length;i++){
int t = i * 1000;
threads[i] = new Thread(()->{
Connection conn = null;
try {
conn = pool.getConnection();
if(conn != null){
System.out.println(Thread.currentThread().getName()+"获取到连接 "+conn);
Thread.sleep(3000 + t);//模拟每个连接使用时间不等
pool.returnConnToPool(conn);
}else{
System.out.println(Thread.currentThread().getName()+" 没有获取到连接");
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-"+i);
}
for(Thread thread : threads){
thread.start();
}
}
}
测试结果
可以看出,请求数超过池中的最大数时,多余的请求会进入等待状态,等到其他的连接被释放后才会获取到连接,线程0和线程5用的同一个连接,线程1和6用的同一个连接,实现了资源的重复利用,没有在去重新创建和关闭连接,节省了完成这些工作需要的时间,提高了效率。