• 一个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

  • 相关阅读:
    JavaScript操作DOM对象
    QTP(13)
    QTP(12)
    QTP(11)
    QTP(10)
    QTP(9)
    QTP(8)
    QTP(7)
    QTP(6)
    QTP(5)
  • 原文地址:https://www.cnblogs.com/qitian1/p/6463499.html
Copyright © 2020-2023  润新知