这个不是因为闲的没事干,先说下需求背景
我们有一个数据源管理模块,配置的数据源连接,用户名,密码等信息
在数据源管理模块配置好之后,去另一个模块选择数据源,获取每个数据源下库表结构以及字段
之前是每次都会创建一个新的连接,那这肯定会比较慢,反复打开和关闭连接,耗费时间
但是我们平时用的Druid连接池,我研究了一下,似乎没办法我说的这种业务
1.因为Druid虽然支持多数据源,但一般都是支持两个,三个数据源进行切换。
但是我们业务可能达到上百个数据源连接
2.而且Druid是需要在yml文件中,提前配置好你需要哪几个数据库,把连接信息写上
但我们可能自由的修改新增删除,无法在项目启动的时候就知道我有哪些数据源需要操作
这种应该是有开源框架的,但是我没找到,就花了一天时间写了一个
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方法,还加了个定时,每天八点,看哪个连接关了,就新建一个连接
@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()));
}
}
@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;
}
/**
* 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);
}
/**
* 获得数据库连接
* 通过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();
}
}
}
}
/**
* 关闭数据库连接
* 如果当前连接被使用的人数为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);
}
}
用我提供的静态方法获取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());
}
这个是踩了坑才想起来加的
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);
}
});
}
}
}
这个是踩了个坑
Collections.singletonList和Arrays.asList()和正常new 出来的List使用方式有所不同
我开始是新建conn时 就Collections.singletonList了
后来销毁连接时候 removeAll出错了
报了个UnsupportedOperationException
所以我改成了这样
Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1,可以减少内存空间。但是返回的值依然是Collections的内部实现类,同样没有add的方法,调用add,set方法会报错
Arrays.asList返回的集合不支持元素的添加和删除。也就是不可以使用add、addAll和remove操作。