手撕数据库连接池

1.有开源的数据库连接池,你为啥不用?

这个不是因为闲的没事干,先说下需求背景 

我们有一个数据源管理模块,配置的数据源连接,用户名,密码等信息

在数据源管理模块配置好之后,去另一个模块选择数据源,获取每个数据源下库表结构以及字段

之前是每次都会创建一个新的连接,那这肯定会比较慢,反复打开和关闭连接,耗费时间

但是我们平时用的Druid连接池,我研究了一下,似乎没办法我说的这种业务

1.因为Druid虽然支持多数据源,但一般都是支持两个,三个数据源进行切换。

但是我们业务可能达到上百个数据源连接

2.而且Druid是需要在yml文件中,提前配置好你需要哪几个数据库,把连接信息写上

我们可能自由的修改新增删除,无法在项目启动的时候就知道我有哪些数据源需要操作

2.思路

 这种应该是有开源框架的,但是我没找到,就花了一天时间写了一个

1.项目启动的时候,我会加载数据源模块的数据,

以jdbcurl+username为key

用concurrentHashMap为每个数据源创建一个连接  

2.实体结构

2.1先创建一个实体ConnEntity,里边仨属性,一个conn,一个当前conn被几个人使用,一个现在conn是否在被使用状态

 2.2再创建一个实体ConnCacheEntity,这个就是连接池Map的value

里边俩属性 一个是connList,一个是当前访问数

当前访问数是说当前jdbcurl+username为key的情况下有多少个人使用

2.3连接池map

concurrentHashMap

key=jdbcurl+username

value=ConnCacheEntity

 3.思想

连接池就是为了连接可以复用,不需要每次都去创建一个新的连接,在释放的时候不是真正的关闭连接,而是把链接还给池子

在知道连接池思想的前提下,再去说手撕数据库连接池这件事

 4.获得连接

* 通过url username 从内存中获取连接 或者新建连接 放到内存中并且返回
* 通过key 取连接信息 没有的话就新建立一个连接返回,有的话就当前使用数最少的那个返回
* 当前存在可用连接  算出目前访问数和已有连接的比例  <=标准比例 则返回一个现在使用数量最小的连接  >标准比例  创建一个连接  与第一个步骤相同
* 我现在的设计是一个conn可以同时被五个人访问,所以现在如果发现有6个人 那就要新创建一个连接返回了
* 而且不管是返回哪个连接,都要设置这个连接被使用标识为true,被使用次数+1,而且key对应的使用人数+1
* 使用连接之后 需要在finally方法中把当前连接数-1 如果当前连接数-1之后为0  则 当前连接使用标识为false
* 使用连接之后 还要根据url+username组成的key 把当前对应的访问数-1

5.释放连接

释放连接中有些操作就是和获得连接时候相反了

* 如果当前连接被使用的人数为0 则使用标识设置为false
* 同数据库连接池 酌情关闭连接
* 因为五个人用一个连接  所以 人数/连接数=0.2 是目前标准
* 如果目前  人数*0.2=目前应有的连接数
* 如果实际连接数>目前应有连接数 就把目前没有被用到的连接关闭,并且移除数组
* 连接数至少要留一个
* 如果实际连接数>目前应有连接数 算出差了几个 就把目前没有被用到的连接关闭,并且移除数组
* 当前是把连接还给池子  所以池子的访问数量-1
* 当前conn的连接数量-1

6.注意点

mysql 连接空闲8小时会自动断开连接 所以返回连接之前需要检测 如果当前连接关闭 就再建一个连接返回

  发现这一点之后,我不光修改了getConn方法,还加了个定时,每天八点,看哪个连接关了,就新建一个连接

3.项目启动时候初始化连接池

@Component
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@RequiredArgsConstructor
public class CommandLineRunnerImpl implements CommandLineRunner {

    SysDataSourceService sysDataSourceService;
    @Override
    public void run(String... args){
        this.createDBConn();
    }

    /**
     * 程序启动时,对于数据源表中 success状态的 建立连接
     */
    private void createDBConn(){
        List list = sysDataSourceService.list(new LambdaQueryWrapper().eq(SysDataSource::getTestStatus, LinkTestStatus.SUCCESS.name()));
        if(CollectionUtils.isEmpty(list)){
            return;
        }
        list.forEach(source -> ConnFactory.getConn(source.getLinkInfo(),source.getUsername(),source.getPassword()));
    }

}

4.缓存实体

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ConnCacheEntity {

    List connEntityList;

    /**
     * 当前访问数
     * key为url+username
     * 记录当前url+username下的访问数
     * 每次get时 +1 用完 -1
     */
    int linkCount;

    /**
     * url username password用于连接关闭的情况下  再次建立连接时使用
     */
    String url;

    String user;

    String password;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ConnEntity {

    /**
     * 连接对象
     */
    Connection conn;

    /**
     * 当前连接是否正在被使用
     * 每次getConn时 给当前使用对象赋值为true  使用之后 赋值为false
     */
    boolean use;

    /**
     * 使用数量
     * 每次getConn时 +1  使用之后 -1
     */
    int useCount;

    /**
     * 用于连接测试时展示具体失败原因
     */
    SQLException e;
}

5.创建和销毁时公用属性

    /**
     * key是url_username
     * value是对应的连接信息
     * 相当于数据库连接池
     */
    public static Map connMap = new ConcurrentHashMap<>();

    /**
     * 访问数与现在内存中已经建立的数据库连接数的比例
     * 5个人 用一个连接
     */
    private static final BigDecimal LINK_DIVIDE_CONN = BigDecimal.valueOf(5);

    /**
     * 5个人用一个连接 1:5=0.2
     */
    private static final BigDecimal CONN_DIVIDE_LINK = BigDecimal.valueOf(0.2);
    /**
     * 每个数据源,最大能打开的连接数
     */
    private static final int MAX_LINK = 10;

    /**
     * key的组成方式 url_username
     *
     * @param url
     * @param user
     * @return
     */
    private static String getKey(String url, String user) {
        return url.concat(Constant.UNDERLINE).concat(user);
    }

6.创建连接

    /**
     * 获得数据库连接
     * 通过url username 从内存中获取连接 或者新建连接 放到内存中并且返回
     * 使用连接之后 需要在finally方法中把当前连接数-1 如果当前连接数-1之后为0  则 当前连接使用标识为false
     * 使用连接之后 还要根据url+username组成的key 把当前对应的访问数-1
     *
     * @param url
     * @param user
     * @param password
     * @return
     */
    public static ConnEntity getConn(String url, String user, String password) {
        if (StringUtils.isEmpty(url) || StringUtils.isEmpty(user)) {
            SQLException sqlException = new SQLException("连接信息有误,请检查用户名端口号等信息");
            return ConnEntity.builder().e(sqlException).build();
        }
        //拼接Key
        String key = getKey(url, user);
        //通过key 取连接信息 没有的话就新建立一个连接返回,有的话就当前使用数最少的那个返回
        ConnCacheEntity connCacheEntity = connMap.get(key);
        if (Objects.isNull(connCacheEntity) || CollectionUtils.isEmpty(connCacheEntity.getConnEntityList())) {
            //如果对象不为空但是对应的可用连接数为空
            try {
                Connection conn = DriverManager.getConnection(url, user, password);
                //因为新建的 即将返回  所以 当前连接正在使用 使用数量为1
                ConnEntity connEntity = ConnEntity.builder().conn(conn).use(true).useCount(1).build();
                //因为新建的 设置当前访问数为1
                List connEntities = new ArrayList<>();
                connEntities.add(connEntity);
                connCacheEntity = ConnCacheEntity.builder().connEntityList(connEntities).linkCount(1).url(url).user(user).password(password).build();
                connMap.put(key, connCacheEntity);
                return connEntity;
            } catch (SQLException e) {
                log.error(e.getMessage());
                return ConnEntity.builder().e(e).build();
            }
        } else {
            //当前连接数+1
            connCacheEntity.setLinkCount(connCacheEntity.getLinkCount() + 1);
            //当前存在可用连接  算出目前访问数和已有连接的比例  <=标准比例 则返回一个现在使用数量最小的连接  >标准比例  创建一个连接  与第一个步骤相同
            //或者是当前连接已经达到上限 就不创建新的连接了 就把目前占用最少的那个conn返回
            int linkCount = connCacheEntity.getLinkCount();
            int size = connCacheEntity.getConnEntityList().size();
            BigDecimal divide = new BigDecimal(linkCount).divide(new BigDecimal(size));
            if (divide.compareTo(LINK_DIVIDE_CONN) <= 0 || size >= MAX_LINK) {
                connCacheEntity.getConnEntityList().sort(Comparator.comparing(ConnEntity::getUseCount));
                //正序 第一个是最小值 赋值 当前正在被使用 并且 使用数+1
                ConnEntity connEntity = connCacheEntity.getConnEntityList().get(0);
                //mysql 连接空闲8小时会自动断开连接 所以返回连接之前需要检测 如果当前连接关闭 就再建一个连接返回
                try {
                    if(connEntity.getConn().isClosed()){
                        Connection conn = DriverManager.getConnection(url, user, password);
                        connEntity.setConn(conn);
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
                connEntity.setUse(true);
                connEntity.setUseCount(connEntity.getUseCount() + 1);
                return connEntity;
            } else {
                try {
                    Connection conn = DriverManager.getConnection(url, user, password);
                    //因为新建的 即将返回  所以 当前连接正在使用 使用数量为1
                    ConnEntity connEntity = ConnEntity.builder().conn(conn).use(true).useCount(1).build();
                    connCacheEntity.getConnEntityList().add(connEntity);
                    //新的连接加入到list中并返回
                    return connEntity;
                } catch (SQLException e) {
                    log.error(e.getMessage());
                    return ConnEntity.builder().e(e).build();
                }
            }
        }
    }

7.销毁连接

  /**
     * 关闭数据库连接
     * 如果当前连接被使用的人数为0 则使用标识设置为null
     * 同数据库连接池 酌情关闭连接
     * 因为五个人用一个连接  所以 人数/连接数=0.2 是目前标准
     * 如果目前  人数*0.2=目前应有的连接数
     * 如果实际连接数>目前应有连接数 就把目前没有被用到的连接关闭,并且移除数组
     *
     * @param connEntity
     * @param url
     * @param user
     */
    public static void  closeConn(ConnEntity connEntity, String url, String user) {
        if (Objects.isNull(connEntity) || StringUtils.isEmpty(url) || StringUtils.isEmpty(user)) {
            return;
        }
        //拼接Key
        String key = getKey(url, user);
        //当前是把连接还给池子  所以池子的访问数量-1
        ConnCacheEntity connCache = connMap.get(key);
        connCache.setLinkCount(connCache.getLinkCount() - 1);
        //当前conn的连接数量-1
        int useCount = connEntity.getUseCount();
        if (useCount > 0) {
            connEntity.setUseCount(connEntity.getUseCount() - 1);
        }
        //如果当前连接被使用的人数为0 则使用标识设置为null
        if (connEntity.getUseCount() == 0) {
            connEntity.setUse(false);
        }
        //人数*0.2=目前应有的连接数   如果实际连接数>目前应有连接数 就把目前没有被用到的连接关闭,并且移除数组
        BigDecimal multiply = new BigDecimal(connCache.getLinkCount()).multiply(CONN_DIVIDE_LINK);
        //向上取整  防止2*0.2=0.4<1 然后把最后一个连接关闭的这种情况发生
        int roundedNumber = multiply.setScale(0, RoundingMode.UP).intValue();
        //实际连接数
        int size = connCache.getConnEntityList().size();
        //连接数至少要留一个
        if (size > roundedNumber && size > 1) {
            //如果实际连接数>目前应有连接数 算出差了几个 就把目前没有被用到的连接关闭,并且移除数组
            int subtract = size - roundedNumber;
            List removes = new ArrayList<>();
            for (ConnEntity entity : connCache.getConnEntityList()) {
                if (!entity.isUse() && subtract > 0) {
                    try {
                        entity.getConn().close();
                        removes.add(entity);
                        subtract--;
                    } catch (SQLException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            connCache.getConnEntityList().removeAll(removes);
        }
    }

8.使用方式

用我提供的静态方法获取conn

还是要在finally中去调用我提供的关闭连接的方法

但是不要自己去conn.close();而是调用静态方法由我来决定是否关闭

    private static List getColumnNames(ConnectQueryBO connectQueryBO) {
        // 调用方法并计时
        long startTime = System.currentTimeMillis();
        ConnEntity conn1 = null;
        List list = new ArrayList<>();
        Statement stmt = null;
        ResultSet resultSet = null;
        try {
            // conn = DriverManager.getConnection(connectQueryBO.getUrl(), connectQueryBO.getUser(), connectQueryBO.getPassword());
            conn1 = ConnFactory.getConn(connectQueryBO.getUrl(), connectQueryBO.getUser(), connectQueryBO.getPassword());
            Connection conn = conn1.getConn();
            stmt = conn.createStatement();
            resultSet = stmt.executeQuery(connectQueryBO.getQuerySql());
            //有dba权限情况下才会执行这部
            while (resultSet.next()) {//如果对象中有数据,就会循环打印出来
                String columName = resultSet.getString(COLUMN_NAME);
                String dataType = resultSet.getString(DATA_TYPE);
                list.add(ColumnBO.builder().columnName(columName).dataType(dataType).build());
            }
        } catch (SQLException e) {
            //select TABLE_NAME from all_tables WHERE owner="+dbName 这个语句可能没有权限执行
            log.error(e.getMessage());
        } finally {
            //finally中关闭连接
            ConnFactory.closeConn(conn1, connectQueryBO.getUrl(), connectQueryBO.getUser());
            try {
                if (Objects.nonNull(stmt)) {
                    stmt.close();
                }
                if (Objects.nonNull(resultSet)) {
                    resultSet.close();
                }
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        // 计算执行时间
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        // 打印执行时间
        System.out.println("getColumnNames方法执行时间:" + executionTime + "毫秒");
        return list.stream().distinct().collect(Collectors.toList());
    }

9.定时检验已关闭的连接

这个是踩了坑才想起来加的

No operations allowed after connection closed

mysql如果连接空闲了8小时,会自动关闭

@Configuration
@EnableScheduling
public class ConnCheck {

    /**
     * 每天早上八点,把已经关闭的连接,重新打开
     * 因为mysql的数据库连接空闲8小时会自动断开
     * 在这里提前重新打开,可以加快效率
     */
    @Scheduled(cron = "0 0 8 * * ?")
    public void check() {
        for (Map.Entry entry : ConnFactory.connMap.entrySet()) {
            ConnCacheEntity entity = entry.getValue();
            if (Objects.isNull(entity)) {
                continue;
            }
            List connEntityList = entity.getConnEntityList();
            if (CollectionUtils.isEmpty(connEntityList)) {
                continue;
            }
            connEntityList.forEach(connEntity -> {
                try {
                    boolean closed = connEntity.getConn().isClosed();
                    //如果连接已经关闭 就重新打开
                    if (closed) {
                        Connection conn = DriverManager.getConnection(entity.getUrl(), entity.getUser(), entity.getPassword());
                        connEntity.setConn(conn);
                    }
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

}

10.UnsupportedOperationException

 这个是踩了个坑

Collections.singletonList和Arrays.asList()和正常new 出来的List使用方式有所不同

我开始是新建conn时 就Collections.singletonList了

后来销毁连接时候  removeAll出错了

报了个UnsupportedOperationException

所以我改成了这样

 Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1,可以减少内存空间。但是返回的值依然是Collections的内部实现类,同样没有add的方法,调用add,set方法会报错

Arrays.asList返回的集合不支持元素的添加和删除。也就是不可以使用add、addAll和remove操作。

 

你可能感兴趣的:(数据库)