文章目录
问题
公司自研框架uap,间歇性打不开数据库连接,提示连接已被关闭。
基于公司自研的web框架,数据库为hibernate+c3p0。基于插件式的框架,即每个部分都是一种插件,通过配置来实现。比如底包中的有菜单插件、数据库定义插件、工作流插件、权限插件等。
不同的业务部门,只要使用uap的底包,基于自研框架的约束开发自己的jar包即可。
stage1:复现1
先是根据测试提供流程,进行复现,发现复现不了。然后各种试,不复现。Bug就挂起来延迟了。
stage2: 复现2
经过几天的规律,发现只有在使用我的线程池跑的时候,才会出现这个问题。页面请求的连接正常。
向uap框架负责人咨询,说是没遇见过。可能是线程池。他们都是new Thread
因为我们业务的异步执行很频繁,所以new Thread肯定不是最优解决方案。
开始看他的源码,uap提供了一个DAO类(DAODataMng),所有的数据库操作都要使用这个类,由它作为切入点。
发现它获取连接是在一个本地的缓存类里拿的,有两个静态属性,
- 在ThreadLocal缓存了一个map,k=项目id,v=session,
- 还用ConcurrentHashMap缓存了SessionFactory,k=项目id,v=SessionFactory。
构造方法中,判断缓存中没有,就用SessionFactory创建一个session放进去,每次用这个session来openConnection,而open的时候,就是从c3p0池子里拿连接。
一路找下去,没找到问题,因为页面请求过来的也能正常用,所以开始看c3p0的配置。
开始看db的c3p0配置,从c3p0.max_size
、c3p0.timeout
看,
- 是不是超时被关了?调大timeout为1个小时,仅仅为了验证问题;(未解决)
- 是不是连接太多,被mysql关了?调大mysql 最大连接参数,和调小c3p0的最大连接参数(未解决)
- 为什么正常的tomcat线程就没问题,我自己的线程池就有问题? 是不是连接有过期时间?(问题关键)
终于,在另一个配置文件里,找到了c3p0.maxConnectionAge=300
,连接只保留5分钟。
那么他们肯定在源码中,每个请求之后,都会把ThreadLocal的缓存清掉,下次再重建。
经过跟自研框架开发确认,他们确实会在每个请求结束后清掉这部分缓存。
确定问题:
因为ThreadLocal缓存了Session,连接配了超时时间5分钟。 线程池的缓存不会被刷新,肯定会等到超时,导致连接失效。
解决方法:
很简单,就是在每次线程池任务跑之前和跑之后都清一遍缓存。
跑之前清,是为了最大限度避免问题
当然了,可以封装一个ExecutorService,用来包装普通的ThreadPoolExecutor。每次跑之前和跑之后清掉ThreadLocal中的缓存。
至此,已经解决了这个问题。
stage3:又发生
过了一段时间,又出现了连接被关闭的问题,😐 …
复现
经过多次验证,在导入数据和线程池任务同时跑的时候,线程池的任务就断了。100%复现。
先是看这段时间新加的代码,都是使用的框架操作的数据库,
看数据库服务器、内存、连接、错误日志;
- 修改hibernate的c3p0池子配置,添加
c3p0.testConnectionOnCheckout=true
,每次拿连接前进行检验(未解决)。 - 测试服务器没内存了,怀疑mysql杀的连接,把mysql嵌套另一台服务器。(未解决)
最后通过log发现规律,每次都在pubDB日志打印之后,会出现这个问题。
(导入数据过程中,是要自动建表,调用框架的给的pubDB方法,用来发布表定义的。)
发现问题:看源码,pubDB后,会关掉sessionFactory(导致所有连接都被关了)和当前线程的session。 所以相当于pubDB的线程已经清掉缓存了,所以拿到的肯定是最新的,而其它线程的已经都被关了,确不知道。
解决方案:
与uap开发负责人沟通,他们pubDB只在项目上线前,由实施人员或者开发使用supersa账号,进行建表和发布,运行过程中不会动态建表,这个方法是因为我们业务需要,他们开发给放开给我们的。
所以,需要做到在pubDB的时候,不能操作数据库,在操作数据库的时候不能pubDB,从而做到互斥,并且在factory更新之后,其它线程要获取到这个状态,并及时的刷新当前线程的Session缓存。
接下来,开始设计方案
开发方案:
pubDB肯定需要加锁,同时只有1个线程操作,而数据库操作(DAODataMng),肯定不能只能1个线程操作。 并且,pubDB和数据库操作是要互斥的。
- 读写锁(ReentrantReadWriteLock),巧妙的符合我们的需求,给pubDB加写锁,给数据库操作加读锁。
并且在获取到读锁之后,需要判断一下当前缓存里的session中的factory是否被关闭了,如果被关闭了,肯定就是发布过了,就需要清一下线程中的session了。
(读写锁:读加共享锁,写加互斥锁。 允许同时多个读,不允许同时多个写,也不允许同时读写) - DAODataMng是自研框架底包里的,我们不能改,所以只能包装,或者代理。包装的话,如果底包改了方法,或者新加了方法,我们还需要改。所以我使用了cglib动态代理,所有的DAODataMng操作,使用代理类代替。
新增工厂类,直接获取代理对象。 - pubDB是uap部门提供给我们的一个静态方法,而具体逻辑是在里边的一个private方法中,cglib代理不到。 既然代理不了,那就把他给改了。 我使用javassist去修改这个方法字节码,先加写锁,finally去释放锁。
代码实现:
DAODataMngFactory: 创建DAODataMng代理类,修改pubDb字节码;
因为我们要根据项目id判断当前线程中的当前项目下的sessionFactory,所以需要拿到项目id属性,用来加读锁。
package net.a.cdl_plugins.core.proxy;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import net.a.uap.dbdatamng.DAODataMng;
import net.sf.cglib.proxy.Enhancer;
public class DAODataMngFactory {
public static DAODataMng createDataMng(String projectId){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(DAODataMng.class);
enhancer.setCallback(new DAODataMngProxy());
return (DAODataMng) enhancer.create(new Class[]{String.class},new Object[]{projectId});
}
private static boolean pubDbFlag = false;
public synchronized static void replacePubDB() {
if(pubDbFlag) return;
try {
ClassPool classPool = ClassPool.getDefault();
classPool.appendSystemPath();
try {
String path = DAODataMngFactory.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
// 因为改的字节码时UAP包里的,所以需要加载UAP包。
String targetJarPath = path.substring(0, path.lastIndexOf('/') + 1) + "***UAP.jar";
System.out.println(targetJarPath);
classPool.appendClassPath(path);
classPool.appendClassPath(targetJarPath);
} catch (Exception e) {
e.printStackTrace();
}
CtClass ctClass = classPool.get("net.***.uap.dbcore.dbpub.DAODbPub");
//classPool.importPackage("");
// 获取 pubDB 方法
CtMethod pubDBMethod = ctClass.getDeclaredMethod("doPub");
// 构建新的方法代码
// 在方法执行前插入代码
String beforeCode = "{ net.***.cdl_plugins.core.proxy.DataMngLock.lockPubDB(); }" ;
String afterCode = "{ net.***.cdl_plugins.core.proxy.DataMngLock.releasePubDB(); }";
pubDBMethod.insertBefore(beforeCode);
pubDBMethod.insertAfter(afterCode,true);
// 保存修改后的类
ctClass.writeFile();
ctClass.toClass();
System.out.println("Modified DAODbPub class successfully.");
pubDbFlag = true;
} catch (Exception e) {
e.printStackTrace();
System.out.println("Modified DAODbPub class error.");
}
}
}
DAODataMngProxy,cglib动态代理拦截器
package net.***.cdl_plugins.core.proxy;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class DAODataMngProxy implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
try {
Field pidField = o.getClass().getSuperclass().getDeclaredField("projectId");
pidField.setAccessible(true);
String projectId = (String) pidField.get(o);
DataMngLock.lockDataMng(projectId);
return methodProxy.invokeSuper(o, objects);
} finally {
DataMngLock.releaseDataMng();
}
}
}
DataMngLock 读写锁工具类
package net.***.cdl_plugins.core.proxy;
import net.***.uap.dbcp.DBSessFactory;
import net.***.webio.dbmanage.DBConnPool;
import net.***.webutil.tools.Log;
import org.hibernate.Session;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
*
*/
public class DataMngLock {
private static final ReadWriteLock lock = new ReentrantReadWriteLock(true);
public static void lockDataMng(String projectId){
lock.readLock().lock();
try {
// 判断缓存中的sessionFactory是否被关了,如果被关了就清缓存。
Session session = new DBSessFactory(projectId).open();
if (session == null || session.getSessionFactory().isClosed()) {
DBSessFactory.close();
DBConnPool.close();
}
} catch (Exception e) {
Log.error("", e);
}
Log.info(Thread.currentThread().getName()+" data mng acquired lock ...");
}
public static void releaseDataMng(){
Log.info(Thread.currentThread().getName()+" data mng release lock ...");
lock.readLock().unlock();
}
public static void lockPubDB(){
lock.writeLock().lock();
Log.info(Thread.currentThread().getName()+" pub db acquired lock ...");
}
public static void releasePubDB(){
Log.info(Thread.currentThread().getName()+" pub db release lock ...");
lock.writeLock().unlock();
}
static class Log{
public static void info(String msg){
//System.out.println(msg);
}
public static void error(String msg,Exception t){
net.***.webutil.tools.Log.error(msg,t);
}
}
}