• 一个Web报表项目的性能分析和优化实践(二):MySQL数据库连接不够用(TooManyConnections)问题的一次分析和解决案例


    最近,项目中遇到了数据库连接不够的问题。

    异常信息
    com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException:
     Data source rejected establishment of connection,  message from server: "Too many connections"
     
     根据更详细的错误信息,我定位到报错的函数位置。


      关键函数
      

     /**
      * 判断数据库是否存在
      */
     public boolean hasDatabaseByKey(String name) {
      boolean containsKey = dataSourceMap.containsKey(name);
      if (containsKey) {
    
       try {
           //根据数据库名称,把BoneCP数据源中的数据库参数取出来,用户名、密码、URL
        Object obj = dataSourceMap.get(name);
        com.jolbox.bonecp.BoneCPDataSource dataSource = (com.jolbox.bonecp.BoneCPDataSource) obj;
    
        String password = dataSource.getPassword();
        String username = dataSource.getUsername();
        String url = dataSource.getJdbcUrl();
        //建立新的数据库连接--这一行代码抛出“Too many connections”异常
        Connection con = DriverManager.getConnection(url, username, password);
        //关闭连接
        if (con != null) {
         con.close();
        }
        return true;
    
       } catch (Exception e) {
        LOG.error("Database name error:" + name);
        e.printStackTrace();
       }
      }
      return false;
     }


     

    是否正常关闭了数据库连接
    上述代码的主要功能是,根据前端传入的数据库名字,如“test”,检测该数据库的配置是否正确。
     
    最初想到的,上面的数据库连接con可能没有正常关闭。
    经过单步跟踪debug,和使用MySQL的 show processlist命令,发现所有的连接确实是正常关闭的。
    因此,上述代码的功能是没有问题的。

    功能没有问题,但是上述代码还是存在其它问题的
    a.数据库连接应该在finally里关闭。
    b.这种检测方法,每次都需要打开一个连接,比较耗费时间。
    一种好的方法是,在系统初始化的时候,检查所有的数据源配置是否正确,把结果
    用Map保存起来,("test",true)表明test数据库配置正确;
    或者每一次检查,先从缓存中取,如果存在直接使用,否则,打开连接,进行检查,然后保存结果到缓存中。

    问题根源
    项目中使用了多个数据库, 数据源公共的配置如下。
       

      <bean id="abstractDataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
            <property name="username" value="root" />
      <property name="password" value="xxxx" />
            <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
             <property name="maxConnectionsPerPartition" value="50"/>
      <property name="minConnectionsPerPartition" value="2"/> 
        </bean>
     



    最近新增了10个数据源配置。
    也就是说,现在新增的数据库连接,最高可以达到50*(10+1)=550个了。(不是50*1=50个)

    而数据库MySQL的默认max_connections是100。
    因此,我们在访问Web项目,然后频繁切换项目的时候,数据库连接池中的数目,已经达到了100。

    这个时候,我们再去手动创建数据库连接,就会失败。

    上文代码的更多配置信息

    @Resource
    private Map<String, Object> dataSourceMap;


    <!-- 配置多数据源映射关系 -->
     <bean id="dataSourceMap" class="java.util.HashMap">
        <constructor-arg>
          <map key-type="java.lang.String">
           <entry key="demo1">
            <bean parent="abstractDataSource" >
          <property name="jdbcUrl"
          value="jdbc:mysql://ip:3306/demo1?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull" />
         </bean>
           </entry>
            
           <entry key="demo2">
         <bean parent="abstractDataSource" >
          <property name="jdbcUrl"
          value="jdbc:mysql://ip:3306/demo2?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull" />
         </bean>
           </entry>
          </map>
        </constructor-arg>
      </bean>



    改进后的代码

    private static Map<String, Boolean> databaseStatusMap = new HashMap<String, Boolean>();
      /**
      * 判断数据库是否存在
      */
     public boolean hasDatabaseByKey(String name) {
      //数据源中是否含有这个数据库名称
      boolean containsKey = dataSourceMap.containsKey(name);
      //不存在这个数据名字,直接false
      if (!containsKey) {
       return false;
      }
    
      //首先从缓存中拿,不为null,表明已经验证过了
      Boolean status=databaseStatusMap.get(name);
      if(status != null){
       return status;
      }
      
      //缓存中不存在,第1次验证
      boolean databaseConfigSucceed = false;
      Connection con = null;
      try {
       Object obj = dataSourceMap.get(name);
       com.jolbox.bonecp.BoneCPDataSource dataSource = (com.jolbox.bonecp.BoneCPDataSource) obj;
    
       String password = dataSource.getPassword();
       String username = dataSource.getUsername();
       String url = dataSource.getJdbcUrl();
    
       //验证数据库连接,把结果存到缓存中
       con = DriverManager.getConnection(url, username, password);
       
       //这个地方con不可能为null,要么是一个正常的连接,要么抛出异常
       databaseConfigSucceed = true;
       databaseStatusMap.put(name, true);
       /*
        if (con != null) {
        databaseConfigSucceed = true;
        databaseStatusMap.put(name, true);
       } 
       else{
        databaseConfigSucceed = false;
        databaseStatusMap.put(name, false);
       }*/
      
      } catch (Exception e) {
       //抛出异常,下次不会去重新检查,可能会存在bug,如果第1次检测的时候,网断了或者MySQL挂了或者数据库连接过多,并不能代表配置不正确
       //小概率事件,暂时不考虑
       LOG.error("Database name error:" + name);
       databaseConfigSucceed=false;
       databaseStatusMap.put(name, false);
       e.printStackTrace();
      } finally {
       if (con != null) {
        try {
         //验证完数据库连接,需要手动关闭
         con.close();
        } catch (SQLException e) {
         e.printStackTrace();
        }
       }
      }
      return databaseConfigSucceed;
     }
    
    


    小概率事件

    如果在执行 DriverManager.getConnection(url, username, password);获取数据库连接的时候,发生了异常。

    如果恰好是第1次检测的时候,网断了或者MySQL挂了或者数据库连接过多,并不能代表配置不正确。
    抛出异常,下次就不会去重新检查,可能会存在bug。

    不过,系统第一次访问项目的时候,正好出故障的可能性很低。

    可能存在的错误情况:

    第一次成功,加入缓存为true,如果今后数据库连接打不开,应该提示“不可以打开”,也会提示“可以打开”。

    第一次不成功,加入缓存为false,如果今后数据库能够打开,应该提示“可以打开”,也会提示“不可以打开”。

    结论

    1.在不使用缓存的情况下,每次都通过建立新连接的方式。

       优点:可以准确的判断数据库配置是否正确,是否真正能够连接到数据库,代码的可读性和复杂度比较低。

      缺点:性能比较差。

    2.使用缓存。

       优点:性能比较高。

       缺点:在不正常情况下,准确性没有保障。代码的可读性和复杂度比较高。

    再次改进

    缓存,增加“时间限制”,过一段时间后,就失效。

    观点

    实现的功能越多,越准确,性能越好,代码可能会越来越复杂。

    代码的正确性和性能有的时候是“互相排斥”的。

    过多的追求完美,也会带来一些负担。

    原文参见: http://FansUnion.cn/articles/2961

  • 相关阅读:
    java.lang.NoSuchMethodError
    asm相关内容想下载(包括 jar 包)
    Initialization of bean failed; nested exception is java.lang.NoClassDefFoundError: org/objectweb/asm/Type
    用Navicat连接mysql报错:2003-Can't connect to MySql server on '10.100.0.109'(10039)
    The type java.lang.reflect.AnnotatedElement cannot be resolved. It is indirectly referenced from required .class files
    The type java.lang.CharSequence cannot be resolved. It is indirectly referenced from required .class files
    交通测速方式
    卡口和电子警察的区别
    Myeclipse连接Mysql数据库时报错:Error while performing database login with the pro driver:unable
    在window上安装mysql
  • 原文地址:https://www.cnblogs.com/qitian1/p/6463500.html
Copyright © 2020-2023  润新知