记一次自研框架Cannot open connection :has been closed() -- you can no longer use it.问题,c3p0、cglib、javassist

问题

公司自研框架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_sizec3p0.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和数据库操作是要互斥的。

  1. 读写锁(ReentrantReadWriteLock),巧妙的符合我们的需求,给pubDB加写锁,给数据库操作加读锁
    并且在获取到读锁之后,需要判断一下当前缓存里的session中的factory是否被关闭了,如果被关闭了,肯定就是发布过了,就需要清一下线程中的session了。
    (读写锁:读加共享锁,写加互斥锁。 允许同时多个读,不允许同时多个写,也不允许同时读写)
  2. DAODataMng是自研框架底包里的,我们不能改,所以只能包装,或者代理。包装的话,如果底包改了方法,或者新加了方法,我们还需要改。所以我使用了cglib动态代理,所有的DAODataMng操作,使用代理类代替。
    新增工厂类,直接获取代理对象。
  3. 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);
        }

    }


}

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值