从零开始实现Java多线程数据库连接池(附一个神秘的问题)

本例采用mysql数据库,因此请先下载mysql-connection.jar

在我们的实际开发中,离不开和数据库打交道。而和数据库的通信,离不开数据库连接。
通常用JDBC连接数据库时,需要加载数据驱动,然后再通过接口返回数据库连接。
一般分为两步:
1、加载驱动至内存
Class.forName(“com.mysql.jdbc.Driver”);

2、创建并获取连接,返回的是JDBC中的Connection
DriverManager.getConnection(url, user, password)

示例:

//要连接的数据库URL
String url = "jdbc:mysql://localhost:3306/tangwmdb";
//连接的数据库时使用的用户名
String username = "root";
//连接的数据库时使用的密码
String password = "root";

//1.加载驱动
//DriverManager.registerDriver(new com.mysql.jdbc.Driver());不推荐使用这种方式来加载驱动
Class.forName("com.mysql.jdbc.Driver");//推荐使用这种方式来加载驱动

//2.获取与数据库的链接
Connection conn = DriverManager.getConnection(url, username, password);

//3.获取用于向数据库发送sql语句的statement
Statement st = conn.createStatement();

String sql = "select id,name,password from members";
//4.向数据库发sql,并获取代表结果集的resultset
ResultSet rs = st.executeQuery(sql);

//5.取出结果集的数据
while(rs.next()){
    System.out.println("id=" + rs.getObject("id"));
    System.out.println("name=" + rs.getObject("name"));
    System.out.println("password=" + rs.getObject("password"));
}

//6.关闭链接,释放资源
rs.close();
st.close();
conn.close();

众所周知,创建数据库连接需要消耗较多的资源,且创建时间也较长。如果网站一天100万PV(假设每个页面都有DB读取或修改操作),程序就需要创建100万次连接,极大的浪费资源。
事实上,同一时间需要创建数据库连接的请求数量并不多,一般几百个足够了。那么我们可以根据需要创建一个连接池,它负责分配、管理和释放数据库连接,它允许应用程序重复使用同一个现有的数据库连接,而不是重新建立一个。这里用到了设计模式中的一个模式:享元模式(Flyweight)
比如我们的连接池中有1000条连接,请求来时,连接池从池中分配一条给请求,用完后收回,而不是销毁,等到下次有请求来时,又可以重复分配使用。
从零开始实现Java多线程数据库连接池(附一个神秘的问题)_第1张图片

当使用了数据库连接池之后,在项目的实际开发中就不需要编写连接数据库的代码了,直接从数据源获得数据库的连接。比如:

//DBCP 数据库连接池
DataSource ds = BasicDataSourceFactory.createDataSource(prop);
Connection conn = ds.getConnection();

可以看到创建连接的工作很简单,因为复杂的分配、回收功能都交给了连接池去处理。

当前有一些开源的数据连接池实现:

  • DBCP 数据库连接池
  • C3P0 数据库连接池

另外阿里开源项目Druid(整个项目由数据库连接池、插件框架和SQL解析器组成)中的数据库连接池被很多互联网公司都采用在生产环境中。

编写自己的数据库连接池

编写的连接池需要做到以下几个基本点:
1、可配置并管理多个连接节点的连接池
从零开始实现Java多线程数据库连接池(附一个神秘的问题)_第2张图片

2、始使化时根据配置中的初始连接数创建指定数量的连接
3、在连接池没有达到最大连接数之前,如果有可用的空闲连接就直接使用空闲连接,如果没有,就创建新的连接。
4、当连接池中的活动连接数达到最大连接数,新的请求进入等待状态,直到有连接被释放。
5、由于数据库连接闲置久了会超时关闭,因此需要连接池采用机制保证每次请求的连接都是有效可用的。
6、线程安全
7、连接池内部要保证指定最小连接数量的空闲连接。
对于最小连接数在实际应用中的效果以及与初始连接数的区别,其实理解的不是很透。在程序中我采用的方式是,如果 活动连接数 + 空闲连接数 < 最小连接数,就补齐对应数量(最小连接数 - 活动连接数 - 空闲连接数)的空闲连接

摘录一段:

数据库连接池的最小连接数和最大连接数的设置要考虑到以下几个因素:
最小连接数:是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费。
最大连接数:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作。
如果最小连接数与最大连接数相差很大,那么最先连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接。不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是超时后被释放。

系统结构:

1.连接池接口IConnectionPool:里面定义一些基本的获取连接的一些方法。
2.连接池接口实现ConnectionPool
3.连接池管理DBConnectionManager:管理不同的连接池,所有的连接都是通过这里获得。
4.其它工具类,诸如属性读取类PropertiesManager,属性保存类DBPropertyBean。
从零开始实现Java多线程数据库连接池(附一个神秘的问题)_第3张图片

工程结构:

从零开始实现Java多线程数据库连接池(附一个神秘的问题)_第4张图片

工程代码:

DBPropertyBean.java

package com.twm.TDBConnectionPool;
public class DBPropertyBean {

    private String nodeName;
    //数据连接驱动
    private String driverName;
    //数据连接url
    private String url;
    //数据连接username
    private String username;
    //数据连接密码
    private String password;
    //连接池最大连接数
    private int maxConnections ;
    //连接池最小连接数
    private int minConnections;
    //连接池初始连接数
    private int initConnections;
    //重连间隔时间 ,单位毫秒
    private int conninterval ;
    //获取连接超时时间 ,单位毫秒,0永不超时
    private int timeout ;

    //构造方法
    public DBPropertyBean(){
        super();
    }

    //下面是getter and setter

    /**
     * 获取数据库连接节点名称
     * @return
     */
    public String getNodeName() {
        return nodeName;
    }

    /**
     * 设置数据库连接节点名称
     * @param nodeName
     */
    public void setNodeName(String nodeName) {
        this.nodeName = nodeName;
    }

    /**
     * 获取数据库驱动
     * @return
     */
    public String getDriverName() {
        return driverName;
    }

    /**
     * 设置数据库驱动
     * @param driverName
     */
    public void setDriverName(String driverName) {
        this.driverName = driverName;
    }

    /**
     * 获取数据库url
     * @return
     */
    public String getUrl() {
        return url;
    }

    /**
     * 设置数据库url
     * @param url
     */
    public void setUrl(String url) {
        this.url = url;
    }

    /**
     * 获取用户名
     * @return
     */
    public String getUsername() {
        return username;
    }

    /**
     * 设置用户名
     * @param username
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * 获取数据库连接密码
     * @return
     */
    public String getPassword(){
        return password;
    }

    /**
     * 设置数据库连接密码
     * @param password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * 获取最大连接数
     * @return
     */
    public int getMaxConnections() {
        return maxConnections;
    }

    /**
     * 设置最大连接数
     * @param maxConnections
     */
    public void setMaxConnections(int maxConnections) {
        this.maxConnections = maxConnections;
    }

    /**
     * 获取最小连接数(也是数据池初始连接数)
     * @return
     */
    public int getMinConnections() {
        return minConnections;
    }

    /**
     * 设置最小连接数(也是数据池初始连接数)
     * @param minConnections
     */
    public void setMinConnections(int minConnections) {
        this.minConnections = minConnections;
    }

    /**
     * 获取初始加接数
     * @return
     */
    public int getInitConnections() {
        return initConnections;
    }

    /**
     * 设置初始连接数
     * @param initConnections
     */
    public void setInitConnections(int initConnections) {
        this.initConnections = initConnections;
    }

    /**
     * 获取重连间隔时间,单位毫秒
     * @return
     */
    public int getConninterval() {
        return conninterval;
    }

    /**
     * 设置重连间隔时间,单位毫秒
     * @param conninterval
     */
    public void setConninterval(int conninterval) {
        this.conninterval = conninterval;
    }

    /**
     * 获取连接超时时间,单位毫秒
     * @return
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * 设置连接超时时间 ,单位毫秒,0-无限重连
     * @param timeout
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

}

IConnectionPool.java

package com.twm.TDBConnectionPool;

import java.sql.Connection;
import java.sql.SQLException;

public interface IConnectionPool {
    /**
     * 获取一个数据库连接,如果等待超过超时时间,将返回null
     * @return 数据库连接对象
     */
    public Connection getConnection();

    /**
     * 获得当前线程的连接库连接
     * @return 数据库连接对象
     */
    public Connection getCurrentConnecton();

    /**
     * 释放当前线程数据库连接
     * @param conn 数据库连接对象
     * @throws SQLException
     */
    public void releaseConn(Connection conn) throws SQLException;

    /**
     * 销毁清空当前连接池
     */
    public void destroy();

    /**
     * 连接池可用状态
     * @return 连接池是否可用
     */
    public boolean isActive();

    /**
     * 定时器,检查连接池
     */
    public void checkPool();

    /**
     * 获取线程池活动连接数
     * @return 线程池活动连接数
     */
    public int getActiveNum();

    /**
     * 获取线程池空闲连接数
     * @return 线程池空闲连接数
     */
    public int getFreeNum();
}

ConnectionPool.java

package com.twm.TDBConnectionPool;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;


/** 
 * 类说明 :友元类,包内可见,不提供给客户程序直接访问。
 */
class ConnectionPool implements IConnectionPool {

    private static final Logger log = Logger.getLogger(ConnectionPool.class);

    private DBPropertyBean propertyBean=null;

    //连接池可用状态
    private Boolean isActive = true;

    // 空闲连接池 。由于List读写频繁,使用LinkedList存储比较合适
    private LinkedList freeConnections = new LinkedList();  
    // 活动连接池。活动连接数 <= 允许最大连接数(maxConnections)
    private LinkedList activeConnections = new LinkedList(); 

    //当前线程获得的连接
    private ThreadLocal currentConnection= new ThreadLocal();

    //构造方法无法返回null,所以取消掉。在下面增加了CreateConnectionPool静态方法。
    private ConnectionPool(){
        super();
    }

    public static ConnectionPool CreateConnectionPool(DBPropertyBean propertyBean) {
        ConnectionPool connpool=new ConnectionPool();
        connpool.propertyBean = propertyBean;

        //加载驱动 

        //在多节点环境配置下,因为在这里无法判断驱动是否已经加载,可能会造成多次重复加载相同驱动。
        //因此加载驱动的动作,挪到connectionManager管理类中去实现了。
        /*try {
            Class.forName(connpool.propertyBean.getDriverName());
            log.info("加载JDBC驱动"+connpool.propertyBean.getDriverName()+"成功");
        } catch (ClassNotFoundException e) {
            log.info("未找到JDBC驱动" + connpool.propertyBean.getDriverName() + ",请引入相关包");
            return null;
        }*/

        //基本点2、始使化时根据配置中的初始连接数创建指定数量的连接
        for (int i = 0; i < connpool.propertyBean.getInitConnections(); i++) {
            try {
                Connection conn = connpool.NewConnection();
                connpool.freeConnections.add(conn);
            } catch (SQLException | ClassNotFoundException e) {
                log.error(connpool.propertyBean.getNodeName()+"节点连接池初始化失败");
                return null;
            }
        }

        connpool.isActive = true;
        return connpool;
    }



    /**
     * 检测连接是否有效
     * @param 数据库连接对象
     * @return Boolean
     */
    private Boolean isValidConnection(Connection conn) throws SQLException{
        try {
            if(conn==null || conn.isClosed()){
                return false;
            }
        } catch (SQLException e) {
            throw new SQLException(e);
        }
        return true;
    }

    /**
     * 创建一个新的连接
     * @return 数据库连接对象
     * @throws ClassNotFoundException
     * @throws SQLException
     */
    private Connection NewConnection() throws ClassNotFoundException,
            SQLException {

        Connection conn = null;
        try {
            if (this.propertyBean != null) {
                //Class.forName(this.propertyBean.getDriverName());
                conn = DriverManager.getConnection(this.propertyBean.getUrl(),
                        this.propertyBean.getUsername(),
                        this.propertyBean.getPassword());
            }
        } catch (SQLException e) {
            throw new SQLException(e);
        }



        return conn;
    }


    @Override
    public synchronized Connection getConnection() {
        Connection conn = null;
        if (this.getActiveNum() < this.propertyBean.getMaxConnections()) {
            // 分支1:当前使用的连接没有达到最大连接数  
            // 基本点3、在连接池没有达到最大连接数之前,如果有可用的空闲连接就直接使用空闲连接,如果没有,就创建新的连接。
            if (this.getFreeNum() > 0) {
                // 分支1.1:如果空闲池中有连接,就从空闲池中直接获取
                log.info("分支1.1:如果空闲池中有连接,就从空闲池中直接获取");
                conn = this.freeConnections.pollFirst();

                //连接闲置久了也会超时,因此空闲池中的有效连接会越来越少,需要另一个进程进行扫描监测,不断保持一定数量的可用连接。
                //在下面定义了checkFreepools的TimerTask类,在checkPool()方法中进行调用。

                //基本点5、由于数据库连接闲置久了会超时关闭,因此需要连接池采用机制保证每次请求的连接都是有效可用的。
                try {
                    if(this.isValidConnection(conn)){
                        this.activeConnections.add(conn);
                        currentConnection.set(conn);
                    }else{
                        conn = getConnection();//同步方法是可重入锁
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            } else {
                // 分支1.2:如果空闲池中无可用连接,就创建新的连接
                log.info("分支1.2:如果空闲池中无可用连接,就创建新的连接");
                try {
                    conn = this.NewConnection();
                    this.activeConnections.add(conn);
                } catch (ClassNotFoundException | SQLException e) {
                    e.printStackTrace();
                }
            }
        } else {
            // 分支2:当前已到达最大连接数  
            // 基本点4、当连接池中的活动连接数达到最大连接数,新的请求进入等待状态,直到有连接被释放。
            log.info("分支2:当前已到达最大连接数 ");
            long startTime = System.currentTimeMillis();

            //进入等待状态。等待被notify(),notifyALL()唤醒或者超时自动苏醒  
            try{
                this.wait(this.propertyBean.getConninterval());  
            }catch(InterruptedException e) {  
                log.error("线程等待被打断");  
            }

            //若线程超时前被唤醒并成功获取连接,就不会走到return null。
            //若线程超时前没有获取连接,则返回null。
            //如果timeout设置为0,就无限重连。
            if(this.propertyBean.getTimeout()!=0){
                if(System.currentTimeMillis() - startTime > this.propertyBean.getTimeout())  
                    return null;  
            }
            conn = this.getConnection();

        }
        return conn;
    }


    @Override
    public Connection getCurrentConnecton() {
        Connection conn=currentConnection.get();
        try {
            if(! isValidConnection(conn)){
                conn=this.getConnection();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }


    @Override
    public synchronized void releaseConn(Connection conn) throws SQLException {

        log.info(Thread.currentThread().getName()+"关闭连接:activeConnections.remove:"+conn);
        this.activeConnections.remove(conn);
        this.currentConnection.remove();
        //活动连接池删除的连接,相应的加到空闲连接池中
        try {
            if(isValidConnection(conn)){
                freeConnections.add(conn);
            }else{
                freeConnections.add(this.NewConnection());
            }

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        //唤醒getConnection()中等待的线程
        this.notifyAll();
    }

    @Override
    public synchronized void destroy() {
        for (Connection conn : this.freeConnections) {  
            try {
                if (this.isValidConnection(conn)) { 
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }   
        }  
        for (Connection conn : this.activeConnections) {  
            try {
                if (this.isValidConnection(conn)) { 
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            } 
        }
        this.isActive = false;
        this.freeConnections.clear();
        this.activeConnections.clear();
    }

    @Override
    public boolean isActive() {
        return this.isActive;
    }


    @Override
    public void checkPool() {

        final String nodename=this.propertyBean.getNodeName();

        ScheduledExecutorService ses=Executors.newScheduledThreadPool(2);

        //功能一:开启一个定时器线程输出状态
        ses.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(nodename +"空闲连接数:"+getFreeNum());  
                System.out.println(nodename +"活动连接数:"+getActiveNum());   

            }
        }, 1, 1, TimeUnit.SECONDS);

        //功能二:开启一个定时器线程,监测并维持空闲池中的最小连接数
        ses.scheduleAtFixedRate(new checkFreepools(this), 1, 5, TimeUnit.SECONDS);
    }

    @Override
    public synchronized int getActiveNum() {
        return this.activeConnections.size();
    }

    @Override
    public synchronized int getFreeNum() {
        return this.freeConnections.size();
    }

    //基本点6、连接池内部要保证指定最小连接数量的空闲连接
    class checkFreepools extends TimerTask {
        private ConnectionPool conpool = null;

        public checkFreepools(ConnectionPool cp) {
            this.conpool = cp;
        }

        @Override
        public void run() {
            if (this.conpool != null && this.conpool.isActive()) {
                int poolstotalnum = conpool.getFreeNum()
                        + conpool.getActiveNum();
                int subnum = conpool.propertyBean.getMinConnections()
                        - poolstotalnum;

                if (subnum > 0) {
                    System.out.println(conpool.propertyBean.getNodeName()
                            + "扫描并维持空闲池中的最小连接数,需补充" + subnum + "个连接");
                    for (int i = 0; i < subnum; i++) {
                        try {
                            conpool.freeConnections
                                    .add(conpool.NewConnection());
                        } catch (ClassNotFoundException | SQLException e) {
                            e.printStackTrace();
                        }
                    }

                }
            }

        }

    }



}

ConnectionManager.java

package com.twm.TDBConnectionPool;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.log4j.Logger;
import com.twm.TDBConnectionPool.util.PropertiesManager;


public class ConnectionManager {
    private final Logger log = Logger.getLogger(ConnectionPool.class);

    private static ConnectionManager dbm = null;

    /** 
     * 加载的驱动器名称集合 
     */  
    private Set drivers = new HashSet(); 

    /**
     * 数据库连接池字典
     * 为每个节点创建一个连接池(可配置多个节点)
     */
    private ConcurrentHashMap pools = new ConcurrentHashMap();



    private ConnectionManager() {
        createPools();
    }


    /**
     * 装载JDBC驱动程序,并创建连接池
     */
    private void createPools() {
        String str_nodenames = PropertiesManager.getProperty("nodename");
        //基本点1、可配置并管理多个连接节点的连接池
        for (String str_nodename : str_nodenames.split(",")) {
            DBPropertyBean dbProperty = new DBPropertyBean();
            dbProperty.setNodeName(str_nodename);

            //验证url配置正确性
            String url = PropertiesManager.getProperty(str_nodename + ".url");
            if (url == null) {
                log.error(str_nodename+"节点的连接字符串为空,请检查配置文件");
                continue;
            }
            dbProperty.setUrl(url);

            //验证driver配置正确性
            String driver = PropertiesManager.getProperty(str_nodename + ".driver");
            if (driver == null) {
                log.error(str_nodename+"节点的driver驱动为空,请检查配置文件");
                continue;
            }
            dbProperty.setDriverName(driver);


            //验证user配置正确性
            String user = PropertiesManager.getProperty(str_nodename + ".user");
            if (user == null) {
                log.error(str_nodename+"节点的用户名设置为空,请检查配置文件");
                continue;
            }
            dbProperty.setUsername(user);


            //验证password配置正确性
            String password = PropertiesManager.getProperty(str_nodename + ".password");
            if (password == null) {
                log.error(str_nodename+"节点的密码设置为空,请检查配置文件");
                continue;
            }
            dbProperty.setPassword(password);

            //验证最小连接数配置正确性
            String str_minconnections=PropertiesManager.getProperty(str_nodename + ".minconnections");
            int minConn;
            try {
                minConn = Integer.parseInt(str_minconnections);
            } catch (NumberFormatException e) {
                log.error(str_nodename + "节点最小连接数设置错误,默认设为5");
                minConn=5;
            }
            dbProperty.setMinConnections(minConn);

            //验证初始连接数配置正确性
            String str_initconnections=PropertiesManager.getProperty(str_nodename + ".initconnections");
            int initConn;
            try {
                initConn = Integer.parseInt(str_initconnections);
            } catch (NumberFormatException e) {
                log.error(str_nodename + "节点初始连接数设置错误,默认设为5");
                initConn=5;
            }
            dbProperty.setInitConnections(initConn);

            //验证最大连接数配置正确性
            String str_maxconnections=PropertiesManager.getProperty(str_nodename + ".maxconnections");
            int maxConn;
            try {
                maxConn = Integer.parseInt(str_maxconnections);
            } catch (NumberFormatException e) {
                log.error(str_nodename + "节点最大连接数设置错误,默认设为20");
                maxConn=20;
            }
            dbProperty.setMaxConnections(maxConn);

            //验证conninterval配置正确性
            String str_conninterval=PropertiesManager.getProperty(str_nodename + ".conninterval");
            int conninterval;
            try {
                conninterval = Integer.parseInt(str_conninterval);
            } catch (NumberFormatException e) {
                log.error(str_nodename + "节点重新连接间隔时间设置错误,默认设为500ms");
                conninterval = 500;
            }
            dbProperty.setConninterval(conninterval);

            //验证timeout配置正确性
            String str_timeout=PropertiesManager.getProperty(str_nodename + ".timeout");
            int timeout;
            try {
                timeout = Integer.parseInt(str_timeout);
            } catch (NumberFormatException e) {
                log.error(str_nodename + "节点连接超时时间设置错误,默认设为2000ms");
                timeout = 2000;
            }
            dbProperty.setTimeout(timeout);

            //创建驱动
            if(!drivers.contains(dbProperty.getDriverName())){
                try {
                    Class.forName(dbProperty.getDriverName());
                    log.info("加载JDBC驱动"+dbProperty.getDriverName()+"成功");
                    drivers.add(dbProperty.getDriverName());
                } catch (ClassNotFoundException e) {
                    log.error("未找到JDBC驱动" + dbProperty.getDriverName() + ",请引入相关包");
                    e.printStackTrace();
                }
            }

            //创建连接池。这里采用同步方法实现的连接池类ConnectionPool。
            //(如果后面我们还有别的实现方式,只需要更改这里就行了。)
            IConnectionPool cp = ConnectionPool.CreateConnectionPool(dbProperty);
            if (cp != null) {
                pools.put(str_nodename, cp);
                cp.checkPool();
                log.info("创建" + str_nodename + "数据库连接池成功");
            } else {
                log.info("创建" + str_nodename + "数据库连接池失败");
            }
        }

    }

    /**
     * 获得单例
     * 
     * @return DBConnectionManager单例
     */
    public synchronized static ConnectionManager getInstance() {
        if (dbm == null) {
            dbm = new ConnectionManager();
        }
        return dbm;
    }

    /**
     * 从指定连接池中获取可用连接
     * 
     * @param poolName要获取连接的连接池名称
     * @return连接池中的一个可用连接或null
     */
    public Connection getConnection(String poolName) {
        IConnectionPool pool =  pools.get(poolName);
        return pool.getConnection();
    }


    /**
     * 回收指定连接池的连接
     * 
     * @param poolName连接池名称
     * @param conn要回收的连接
     */
    public void closeConnection(String poolName, Connection conn) throws SQLException {
        IConnectionPool pool = pools.get(poolName);
        if (pool != null) {
            try {
                pool.releaseConn(conn);
            } catch (SQLException e) {
                log.error("回收"+poolName+"池中的连接失败。");
                throw new SQLException(e);
            }
        }else{
            log.error("找不到"+poolName+"连接池,无法回收");
        }
    }

    /**
     * 关闭所有连接,撤销驱动器的注册
     */
    public void destroy() {
        for (Map.Entry poolEntry : pools.entrySet()) {
            IConnectionPool pool = poolEntry.getValue();
            pool.destroy();
        }
        log.info("已经关闭所有连接");
    }
}

PropertiesManager.java

package com.twm.TDBConnectionPool.util;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Properties;


public class PropertiesManager {
    private static Properties pro = new Properties();

    private PropertiesManager() {

    }

    static {
        try {
            pro.load(PropertiesManager.class.getClassLoader().getResourceAsStream("DB.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String getProperty(String key) {
        return pro.getProperty(key);
    }

    public static String getProperty(String key, String defaultValue) {
        return pro.getProperty(key, defaultValue);
    }

    public static Enumeration propertiesNames() {
        return pro.propertyNames();
    }
}

testpool.java

package com.twm.TDBConnectionPool.run;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import com.twm.TDBConnectionPool.ConnectionManager;
import com.twm.TDBConnectionPool.DBPropertyBean;
import com.twm.TDBConnectionPool.IConnectionPool;
import com.twm.TDBConnectionPool.util.PropertiesManager;


public class testpool {

    /**
     * @param args
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException {

        List threadlist=new ArrayList();
        for(int i=1;i<=3;i++){
            Thread subThread = new Thread(new workrun(i));
            subThread.start();
            threadlist.add(subThread);
        }
        for (Iterator iterator = threadlist.iterator(); iterator.hasNext();) {
            Thread thread = iterator.next();
            thread.join();
        }
        //ConnectionManager.getInstance().destroy();

    }

}
class workrun implements Runnable{
    int i;
    public workrun(int i){
        this.i=i;
    }
    @Override
    public void run() {
        ConnectionManager cm = ConnectionManager.getInstance();

         //1.从数据池中获取数据库连接
        Connection conn = cm.getConnection("default");
        System.out.println("线程 " + Thread.currentThread().getName() + "获得连接:" + conn);

        //模拟查询耗时操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


         //2.获取用于向数据库发送sql语句的statement
        Statement st = null;
        try {
            st = conn.createStatement();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        String sql = "select * from product where id="+i;
        //3.向数据库发sql,并获取代表结果集的resultset
        //4.取出结果集的数据
        ResultSet rs = null;
        try {
            rs = st.executeQuery(sql);
            while(rs.next()){
                System.out.println("productname=" + rs.getObject("productname"));
                //System.out.println("price=" + rs.getObject("price"));
                //System.out.println("cateid=" + rs.getObject("cateid"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }

        //模拟查询耗时操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //5.关闭链接,释放资源
        try {
            rs.close();
            st.close();
            //conn.close();
            cm.closeConnection("default",conn);
        } catch (SQLException e) {
            e.printStackTrace();
        }

    }

}

DB.properties

nodename=default,testdb

default.driver=com.mysql.jdbc.Driver
default.url=jdbc:mysql://localhost:3306/tangwenmingdb
default.user=root
default.password=root
default.maxconnections=10
default.minconnections=5
default.initconnections=2
default.conninterval=500
default.timeout = 5000

testdb.driver=com.mysql.jdbc.Driver
testdb.url=jdbc:mysql://192.168.1.17:3306/test
testdb.user=root
testdb.password=root
testdb.maxconnections=10
testdb.minconnections=2
testdb.initconnections=2
testdb.conninterval= 500
testdb.timeout = 5000

product表结构和数据:

/*!40101 SET NAMES utf8 */;
create table `product` (
    `id` int (10),
    `productname` varchar (150),
    `price` Decimal (12),
    `cateid` int (11)
); 
insert into `product` (`id`, `productname`, `price`, `cateid`) values('1','iphone 8 plus','5700.00','1');
insert into `product` (`id`, `productname`, `price`, `cateid`) values('2','华为G7','1700.00','1');
insert into `product` (`id`, `productname`, `price`, `cateid`) values('3','HTC','1200.00','1');

这里写图片描述

至此,我们用同步方法synchronized实现了一个简单的数据库连接池。大家可以试着调整DB.properties里的参数配置(比如maxconnections,minconnections,initconnections),观察控制台的输出变化。也可以调整程序中的thread.sleep时长观察。


代理Connection

现在来做一个优化,各位一定要看完,这里搞得我很痛苦,也留下一个未解决的问题。希望大家帮忙。
通常,用户在使用完毕数据库连接后会调用conn.close()关闭连接,而不是调用我们程序中的ConnectionManager.closeConnection("default",conn);方法关闭。
为了让conn.close和我们自己写的closeConnection()方法保持一致的效果,就必须要改写connection的close()方法,在这个场景下,很容易想到使用代理方式。这里先采用动态代理方式来实现:

通过改造ConnectionPool类:
1、在ConnectionPool类中添加一个内部InvocationHandler实现类(和checkFreepools类平级的)

class ConProxyHandler implements InvocationHandler{
        private Connection conn;
        private ConnectionPool cp;

        public ConProxyHandler(ConnectionPool cp, Connection conn) {
            this.cp = cp;
            this.conn = conn;
        }

        @Override

        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            if(method.getName().equals("close")){
                cp.releaseConn(this.conn);
                return null;
            }else{
                //如果不是调用close方法,就原样处理
                return method.invoke(this.conn, args);
            }
        }

    }

2、改造ConnectionPool类的NewConnection()方法,在return conn之前,加上一段

if (conn != null) {
    ConProxyHandler handler = new ConProxyHandler(this, conn);
    Connection conn_return = (Connection) Proxy.newProxyInstance(
            ConnectionPool.class.getClassLoader(),
            new Class[] { Connection.class }, handler);
    System.out.println("生成代理类:" + conn_return);
    return conn_return;
}

最后在调用的testpool.java中把

cm.closeConnection("default",conn);

改为:

conn.close();

然后运行,前面都正常,到最后出问题了:
当三个线程分别关闭连接后,空闲连接数加上去了,但活动连接数并没有相应的减少。
心碎。。。。到底哪出问题了呢?

打断点跟踪发现this.activeConnections.remove(conn);这句并没有成功移除conn。
然后开始检查:
1、打印ConProxyHandler类中的cp和ConnectionPool类中CreateConnectionPool()方法里的connpool,发现hashcode是一致的
2、在ConProxyHandler类中的invoke方法中加上对equals方法的处理。发现压根不会进到这里。
3、跟踪到this.activeConnections.remove(conn);
发现this.activeConnections里保存了三个连接类
从零开始实现Java多线程数据库连接池(附一个神秘的问题)_第5张图片

并且传入参数conn的hashcode和this.activeConnections中其中一个连接类是一致的

从零开始实现Java多线程数据库连接池(附一个神秘的问题)_第6张图片

但是运行完this.activeConnections.remove(conn)后到下句,发现this.activeConnections里依然存在com.mysql.jdbc.JDBC4Connection@109cdd5对象。

为什么删不掉呢?
太打击积极性了,也不知道怎么调试查看原因。开始怀疑自己还适合做开发吗?

即然删不掉,那就将this.activeConnections改成hashmap,用key来删掉。
改动ConnectionPool .java如下:

package com.twm.TDBConnectionPool;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;


/** 
 * @author TangWenming  E-mail:soonfly#gmail.com
 * @version 创建时间:2017-5-16 下午5:49:17 
 * 类说明 :友元类,包内可见,不提供给客户程序直接访问。
 */
class ConnectionPool implements IConnectionPool {

    private static final Logger log = Logger.getLogger(ConnectionPool.class);

    private DBPropertyBean propertyBean=null;

    //连接池可用状态
    private Boolean isActive = true;

    // 空闲连接池 。
    private Hashtable freeConnections = new Hashtable();  
    // 活动连接池。
    private Hashtable activeConnections = new Hashtable(); 

    //当前线程获得的连接
    private ThreadLocal currentConnection= new ThreadLocal();

    //构造方法无法返回null,所以取消掉。在下面增加了CreateConnectionPool静态方法。
    private ConnectionPool(){
        super();
    }

    public static ConnectionPool CreateConnectionPool(DBPropertyBean propertyBean) {
        ConnectionPool connpool=new ConnectionPool();
        connpool.propertyBean = propertyBean;

        //加载驱动 

        //在多节点环境配置下,因为在这里无法判断驱动是否已经加载,可能会造成多次重复加载相同驱动。
        //因此加载驱动的动作,挪到connectionManager管理类中去实现了。
        /*try {
            Class.forName(connpool.propertyBean.getDriverName());
            log.info("加载JDBC驱动"+connpool.propertyBean.getDriverName()+"成功");
        } catch (ClassNotFoundException e) {
            log.info("未找到JDBC驱动" + connpool.propertyBean.getDriverName() + ",请引入相关包");
            return null;
        }*/

        //基本点2、始使化时根据配置中的初始连接数创建指定数量的连接
        for (int i = 0; i < connpool.propertyBean.getInitConnections(); i++) {
            try {
                Connection conn = connpool.NewConnection();
                connpool.freeConnections.put(conn.hashCode(), conn);
            } catch (SQLException | ClassNotFoundException e) {
                log.error(connpool.propertyBean.getNodeName()+"节点连接池初始化失败");
                return null;
            }
        }

        connpool.isActive = true;
        return connpool;
    }



    /**
     * 检测连接是否有效
     * @param 数据库连接对象
     * @return Boolean
     */
    private Boolean isValidConnection(Connection conn) throws SQLException{
        try {
            if(conn==null || conn.isClosed()){
                return false;
            }
        } catch (SQLException e) {
            throw new SQLException(e);
        }
        return true;
    }

    /**
     * 创建一个新的连接
     * @return 数据库连接对象
     * @throws ClassNotFoundException
     * @throws SQLException
     */
    private Connection NewConnection() throws ClassNotFoundException,
            SQLException {

        Connection conn = null;
        try {
            if (this.propertyBean != null) {
                //Class.forName(this.propertyBean.getDriverName());
                conn = DriverManager.getConnection(this.propertyBean.getUrl(),
                        this.propertyBean.getUsername(),
                        this.propertyBean.getPassword());
            }
        } catch (SQLException e) {
            throw new SQLException(e);
        }

        if (conn != null) {
            ConProxyHandler handler = new ConProxyHandler(this, conn);
            Connection conn_return = (Connection) Proxy.newProxyInstance(
                    ConnectionPool.class.getClassLoader(),
                    new Class[] { Connection.class }, handler);
            //System.out.println("生成代理类:" + conn_return);
            return conn_return;
        }

        return conn;
    }


    @Override
    public synchronized Connection getConnection() {
        Connection conn = null;
        if (this.getActiveNum() < this.propertyBean.getMaxConnections()) {
            // 分支1:当前使用的连接没有达到最大连接数  
            // 基本点3、在连接池没有达到最大连接数之前,如果有可用的空闲连接就直接使用空闲连接,如果没有,就创建新的连接。
            if (this.getFreeNum() > 0) {
                // 分支1.1:如果空闲池中有连接,就从空闲池中直接获取
                int key = (int) this.freeConnections.keySet().toArray()[0];
                log.info("分支1.1:如果空闲池中有连接,"+Thread.currentThread().getName()+"就从空闲池中直接获取"+key);
                //conn = this.freeConnections.pollFirst();

                conn = this.freeConnections.get(key);
                this.freeConnections.remove(key);


                //连接闲置久了也会超时,因此空闲池中的有效连接会越来越少,需要另一个进程进行扫描监测,不断保持一定数量的可用连接。
                //在下面定义了checkFreepools的TimerTask类,在checkPool()方法中进行调用。

                //基本点5、由于数据库连接闲置久了会超时关闭,因此需要连接池采用机制保证每次请求的连接都是有效可用的。
                try {
                    if(this.isValidConnection(conn)){
                        this.activeConnections.put(conn.hashCode(), conn);
                        currentConnection.set(conn);
                    }else{
                        conn = getConnection();//同步方法是可重入锁
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }

                System.out.println(this.propertyBean.getNodeName() +"空闲连接数:"+getFreeNum());  
                System.out.println(this.propertyBean.getNodeName() +"活动连接数:"+getActiveNum());
            } else {

                try {
                    conn = this.NewConnection();
                    // 分支1.2:如果空闲池中无可用连接,就创建新的连接
                    log.info("分支1.2:如果空闲池中无可用连接,"+Thread.currentThread().getName()+"就创建新的连接"+conn.hashCode());
                    this.activeConnections.put(conn.hashCode(), conn);
                } catch (ClassNotFoundException | SQLException e) {
                    e.printStackTrace();
                }
                System.out.println(this.propertyBean.getNodeName() +"空闲连接数:"+getFreeNum());  
                System.out.println(this.propertyBean.getNodeName() +"活动连接数:"+getActiveNum());
            }
        } else {
            // 分支2:当前已到达最大连接数  
            // 基本点4、当连接池中的活动连接数达到最大连接数,新的请求进入等待状态,直到有连接被释放。
            log.info("分支2:"+Thread.currentThread().getName()+":当前已到达最大连接数 ");
            long startTime = System.currentTimeMillis();

            //进入等待状态。等待被notify(),notifyALL()唤醒或者超时自动苏醒  
            try{
                this.wait(this.propertyBean.getConninterval());  
            }catch(InterruptedException e) {  
                log.error("线程等待被打断");  
            }

            //若线程超时前被唤醒并成功获取连接,就不会走到return null。
            //若线程超时前没有获取连接,则返回null。
            //如果timeout设置为0,就无限重连。
            if(this.propertyBean.getTimeout()!=0){
                if(System.currentTimeMillis() - startTime > this.propertyBean.getTimeout())  
                    return null;  
            }
            conn = this.getConnection();

        }
        return conn;
    }


    @Override
    public Connection getCurrentConnecton() {
        Connection conn=currentConnection.get();
        try {
            if(! isValidConnection(conn)){
                conn=this.getConnection();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }


    @Override
    public synchronized void releaseConn(Connection conn) throws SQLException {

        log.info(Thread.currentThread().getName()+"关闭连接:activeConnections.remove:"+conn.hashCode());
        this.activeConnections.remove(conn.hashCode());
        this.currentConnection.remove();
        //活动连接池删除的连接,相应的加到空闲连接池中
        try {
            if(isValidConnection(conn)){
                freeConnections.put(conn.hashCode(), conn);
            }else{
                Connection newconn = this.NewConnection();
                freeConnections.put(newconn.hashCode(), newconn);
            }

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        System.out.println(this.propertyBean.getNodeName() +"空闲连接数:"+getFreeNum());  
        System.out.println(this.propertyBean.getNodeName() +"活动连接数:"+getActiveNum());
        //唤醒getConnection()中等待的线程
        this.notifyAll();
    }

    @Override
    public synchronized void destroy() {
        for(int key:this.freeConnections.keySet()){
            Connection conn = this.freeConnections.get(key);
            try {
                if (this.isValidConnection(conn)) { 
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        for(int key:this.activeConnections.keySet()){
            Connection conn = this.activeConnections.get(key);
            try {
                if (this.isValidConnection(conn)) { 
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        this.isActive = false;
        this.freeConnections.clear();
        this.activeConnections.clear();
    }

    @Override
    public boolean isActive() {
        return this.isActive;
    }


    @Override
    public void checkPool() {

        /*final String nodename=this.propertyBean.getNodeName();

        ScheduledExecutorService ses=Executors.newScheduledThreadPool(2);

        //功能一:开启一个定时器线程输出状态
        ses.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(nodename +"空闲连接数:"+getFreeNum());  
                System.out.println(nodename +"活动连接数:"+getActiveNum());   

            }
        }, 1, 1, TimeUnit.SECONDS);

        //功能二:开启一个定时器线程,监测并维持空闲池中的最小连接数
        ses.scheduleAtFixedRate(new checkFreepools(this), 1, 5, TimeUnit.SECONDS);*/
    }

    @Override
    public synchronized int getActiveNum() {
        return this.activeConnections.size();
    }

    @Override
    public synchronized int getFreeNum() {
        return this.freeConnections.size();
    }

    //基本点6、连接池内部要保证指定最小连接数量的空闲连接
    class checkFreepools extends TimerTask {
        private ConnectionPool conpool = null;

        public checkFreepools(ConnectionPool cp) {
            this.conpool = cp;
        }

        @Override
        public void run() {
            if (this.conpool != null && this.conpool.isActive()) {
                int poolstotalnum = conpool.getFreeNum()
                        + conpool.getActiveNum();
                int subnum = conpool.propertyBean.getMinConnections()
                        - poolstotalnum;

                if (subnum > 0) {
                    System.out.println(conpool.propertyBean.getNodeName()
                            + "扫描并维持空闲池中的最小连接数,需补充" + subnum + "个连接");
                    for (int i = 0; i < subnum; i++) {
                        try {
                            Connection newconn = conpool.NewConnection();
                            conpool.freeConnections.put(newconn.hashCode(), newconn);
                        } catch (ClassNotFoundException | SQLException e) {
                            e.printStackTrace();
                        }
                    }

                }
            }

        }

    }

    class ConProxyHandler implements InvocationHandler{
        private Connection conn;
        private ConnectionPool cp;

        public ConProxyHandler(ConnectionPool cp, Connection conn) {
            this.cp = cp;
            this.conn = conn;
        }

        @Override

        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            if(method.getName().equals("close")){
                cp.releaseConn(this.conn);
                return this.conn;
            }else{
                        return true;
                    }
                }else {
                    return true;
                }
            }else{
                //如果不是调用close方法,就原样处理
                return method.invoke(this.conn, args);
            }
        }

    }

}

再次运行。。。。又出西西了。发现如果某个线程获取到的是另一个线程使用过的connection,就无法调用close方法正常关闭。

可以把下面这几个参数值改动一下,很快就见效:

default.maxconnections=2
default.minconnections=1
default.initconnections=1

运行程序后,可以看到最后获取到连接的线程,并没能成功调用con.close()。

至此,数据库连接池是成功实现了。
但是最后优化过程中使用代理Connection对象的方式并没有成功。希望大家帮我一起分析一下是什么原因导致的,可以留言,也可站内信给我。主要为了弄明白到底是怎么回事,不甚感激!

你可能感兴趣的:(-------【Mysql】)