多数据源情况下数据库连接数突增的解决办法
程序出现异常的背景和现象
问题背景:最近对对Oracle数据库的监控程序做重构,有两个重构的重点,一个是将原本两个子程序(分别实现定时调用和监控信息入库)合并成一个程序,另一个是将Oracle多数据源信息在程序中写死改为数据源信息从mysql中读,方便数据源信息维护。
上图以任务一为例,流程为:
定时程序每次执行先去访问mysql数据库获取Oracle数据源》创建数据源连接对象》访问Oracle数据库并返回结果。
程序重构完成后部署上线,并新增了一些需要监控的Oracle数据库(此时数据源达到95个),程序运行几个小时后出现异常,有个别数据库出现一段时间的连接数突增,经查看突增的连接用户是此监控程序所采用的用户。见下图:
分析原因
意识到出现连接数异常之后开始对程序进行问题排查,首先查看了数据库连接池配置,
经过分析连接池配置正确,紧接着查看获取连接对象的代码时发现了存在的问题:
queryOracle方法是获取某个数据源并根据相应sql查询监控信息的公共方法,查询完毕后方法本身对Connection,Statement,ResultSet的对象con,st,rs按正确顺序执行了.close()连接释放处理。
由于该程序访问多个oracle数据源且多个任务并发执行且定时任务调用频繁(最高的频率为每30秒调用一次),在极端情况下(网络延时,某一时刻sql查询慢等)会出现数据库在上一次执行访问Oracle的连接未释放而因为新的定时任务已开始执行又new了新的连接的问题,从而造成了连接数突增。
解决问题
分析得出,解决此问题的关键是创建数据源对象的时机和对已创建数据源对象的处理,即如何在复杂的高并发情况下保证数据源不会多次重复创建,通常的两种方法:
一种是静态多数据源配置,在类中用静态代码块的方法初始化连接对象一次,之后的定时程序从该代码块中取连接对象进行操作;
另一种是,把多数据源交给spring框架利用其IoC(控制反转)特性进行连接信息的管理和获取。
这两种都可以解决此次出现的问题,但这两种方法都推荐在代码或者对应的配置文件中写死数据源信息且程序改造相对繁琐,违反了重构这个程序的初衷(数据源信息在mysql数据库中存,定时调用时候取)。
最终经过思考和同事的指导下整合出了如下解决方法:
第一,在类中初始化静态集合类对象dataSourceMap起“暂存”的作用,当程序调用queryOracle的时候要先通过getDataSource获取数据源。例如,第一次初始化“A”数据源则new一个“A”数据源的连接对象并暂存到dataSourceMap,在之后任意的定时任务再访问“A”就先去dataSourceMap中寻找对应的数据源对象并返回,这样就避免了多次初始化连接对象。
第二,在getDataSource采用synchronized并利用其同步锁的机制保证并发情况下任一线程创建数据源连接对象时的互斥性,这就避免了多个任务第一次且同时调用getDataSource可能会产生的同时new数据源连接对象的问题。
经过分析和实践测试,在此优化下任一数据源信息只创建一次连接对象,而之后再使用同一数据源的时候从静态dataSourceMap取而不再初始化新的连接,至此多数据源情况下可能存在的数据库连接数异常的问题得以解决。
运行正常的数据库连接信息截图:
部分代码
//初始化集合作数据源暂存
private static Map<String, DataSource> dataSourceMap = new HashMap<String, DataSource>();
/**
* getDataSource方法操作了类变量因此需要加同步机制防止并发异常
* @param conninfo
* @return
*/
public synchronized static DataSource getDataSource(OracleConnect conninfo) {
DataSource datasource = null;
if (dataSourceMap.containsKey(conninfo.getDbname())) {
datasource = dataSourceMap.get(conninfo.getDbname());
} else {
datasource = new CreateDateSource(conninfo.getDbip(), DRIVER, USER, PASSWORD).getDS();
dataSourceMap.put(conninfo.getDbname(), datasource);
}
return datasource;
}
推荐阅读