低版本Druid连接池+MySQL驱动8.0导致线程阻塞、性能受限


Posted in MySQL onJuly 01, 2021

现象

应用升级MySQL驱动8.0后,在并发量较高时,查看监控打点,Druid连接池拿到连接并执行SQL的时间大部分都超过200ms

对系统进行压测,发现出现大量线程阻塞的情况,线程dump信息如下:

"http-nio-5366-exec-48" #210 daemon prio=5 os_prio=0 tid=0x00000000023d0800 nid=0x3be9 waiting for monitor entry [0x00007fa4c1400000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader.loadClass(TomcatEmbeddedWebappClassLoader.java:66)
        - waiting to lock <0x0000000775af0960> (a java.lang.Object)
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1186)
        at com.alibaba.druid.util.Utils.loadClass(Utils.java:220)
        at com.alibaba.druid.util.MySqlUtils.getLastPacketReceivedTimeMs(MySqlUtils.java:372)

根因分析

public class MySqlUtils {

    public static long getLastPacketReceivedTimeMs(Connection conn) throws SQLException {
        if (class_connectionImpl == null && !class_connectionImpl_Error) {
            try {
                class_connectionImpl = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
            } catch (Throwable error){
                class_connectionImpl_Error = true;
            }
        }

        if (class_connectionImpl == null) {
            return -1;
        }

        if (method_getIO == null && !method_getIO_error) {
            try {
                method_getIO = class_connectionImpl.getMethod("getIO");
            } catch (Throwable error){
                method_getIO_error = true;
            }
        }

        if (method_getIO == null) {
            return -1;
        }

        if (class_MysqlIO == null && !class_MysqlIO_Error) {
            try {
                class_MysqlIO = Utils.loadClass("com.mysql.jdbc.MysqlIO");
            } catch (Throwable error){
                class_MysqlIO_Error = true;
            }
        }

        if (class_MysqlIO == null) {
            return -1;
        }

        if (method_getLastPacketReceivedTimeMs == null && !method_getLastPacketReceivedTimeMs_error) {
            try {
                Method method = class_MysqlIO.getDeclaredMethod("getLastPacketReceivedTimeMs");
                method.setAccessible(true);
                method_getLastPacketReceivedTimeMs = method;
            } catch (Throwable error){
                method_getLastPacketReceivedTimeMs_error = true;
            }
        }

        if (method_getLastPacketReceivedTimeMs == null) {
            return -1;
        }

        try {
            Object connImpl = conn.unwrap(class_connectionImpl);
            if (connImpl == null) {
                return -1;
            }

            Object mysqlio = method_getIO.invoke(connImpl);
            Long ms = (Long) method_getLastPacketReceivedTimeMs.invoke(mysqlio);
            return ms.longValue();
        } catch (IllegalArgumentException e) {
            throw new SQLException("getLastPacketReceivedTimeMs error", e);
        } catch (IllegalAccessException e) {
            throw new SQLException("getLastPacketReceivedTimeMs error", e);
        } catch (InvocationTargetException e) {
            throw new SQLException("getLastPacketReceivedTimeMs error", e);
        }
    }

MySqlUtils中的getLastPacketReceivedTimeMs()方法会加载com.mysql.jdbc.MySQLConnection这个类,但在MySQL驱动8.0中类名改为com.mysql.cj.jdbc.ConnectionImpl,所以MySQL驱动8.0中加载不到com.mysql.jdbc.MySQLConnection

getLastPacketReceivedTimeMs()方法实现中,如果Utils.loadClass("com.mysql.jdbc.MySQLConnection")加载不到类并抛出异常,会修改变量class_connectionImpl_Error,下次调用不会再进行加载

public class Utils {

    public static Class<?> loadClass(String className) {
        Class<?> clazz = null;

        if (className == null) {
            return null;
        }

        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            // skip
        }

        ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader();
        if (ctxClassLoader != null) {
            try {
                clazz = ctxClassLoader.loadClass(className);
            } catch (ClassNotFoundException e) {
                // skip
            }
        }

        return clazz;
    }

但是,在Utils的loadClass()方法中同样catch了ClassNotFoundException,这就导致loadClass()在加载不到类的时候,并不会抛出异常,从而会导致每调用一次getLastPacketReceivedTimeMs()方法,就会加载一次MySQLConnection这个类

线程dump信息中可以看到是在调用TomcatEmbeddedWebappClassLoader的loadClass()方法时,导致线程阻塞的

public class TomcatEmbeddedWebappClassLoader extends ParallelWebappClassLoader {

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
  synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
   Class<?> result = findExistingLoadedClass(name);
   result = (result != null) ? result : doLoadClass(name);
   if (result == null) {
    throw new ClassNotFoundException(name);
   }
   return resolveIfNecessary(result, resolve);
  }
 }

这是因为TomcatEmbeddedWebappClassLoader在加载类的时候,会加synchronized锁,这就导致每调用一次getLastPacketReceivedTimeMs()方法,就会加载一次com.mysql.jdbc.MySQLConnection,而又始终加载不到,在加载类的时候会加synchronized锁,所以会出现线程阻塞,性能下降的现象

getLastPacketReceivedTimeMs()方法调用时机

public abstract class DruidAbstractDataSource extends WrapperAdapter implements DruidAbstractDataSourceMBean, DataSource, DataSourceProxy, Serializable {

    protected boolean testConnectionInternal(DruidConnectionHolder holder, Connection conn) {
        String sqlFile = JdbcSqlStat.getContextSqlFile();
        String sqlName = JdbcSqlStat.getContextSqlName();

        if (sqlFile != null) {
            JdbcSqlStat.setContextSqlFile(null);
        }
        if (sqlName != null) {
            JdbcSqlStat.setContextSqlName(null);
        }
        try {
            if (validConnectionChecker != null) {
                boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
                long currentTimeMillis = System.currentTimeMillis();
                if (holder != null) {
                    holder.lastValidTimeMillis = currentTimeMillis;
                    holder.lastExecTimeMillis = currentTimeMillis;
                }

                if (valid && isMySql) { // unexcepted branch
                    long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
                    if (lastPacketReceivedTimeMs > 0) {
                        long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs;
                        if (lastPacketReceivedTimeMs > 0 //
                                && mysqlIdleMillis >= timeBetweenEvictionRunsMillis) {
                            discardConnection(holder);
                            String errorMsg = "discard long time none received connection. "
                                    + ", jdbcUrl : " + jdbcUrl
                                    + ", jdbcUrl : " + jdbcUrl
                                    + ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;
                            LOG.error(errorMsg);
                            return false;
                        }
                    }
                }

                if (valid && onFatalError) {
                    lock.lock();
                    try {
                        if (onFatalError) {
                            onFatalError = false;
                        }
                    } finally {
                        lock.unlock();
                    }
                }

                return valid;
            }

            if (conn.isClosed()) {
                return false;
            }

            if (null == validationQuery) {
                return true;
            }

            Statement stmt = null;
            ResultSet rset = null;
            try {
                stmt = conn.createStatement();
                if (getValidationQueryTimeout() > 0) {
                    stmt.setQueryTimeout(validationQueryTimeout);
                }
                rset = stmt.executeQuery(validationQuery);
                if (!rset.next()) {
                    return false;
                }
            } finally {
                JdbcUtils.close(rset);
                JdbcUtils.close(stmt);
            }

            if (onFatalError) {
                lock.lock();
                try {
                    if (onFatalError) {
                        onFatalError = false;
                    }
                } finally {
                    lock.unlock();
                }
            }

            return true;
        } catch (Throwable ex) {
            // skip
            return false;
        } finally {
            if (sqlFile != null) {
                JdbcSqlStat.setContextSqlFile(sqlFile);
            }
            if (sqlName != null) {
                JdbcSqlStat.setContextSqlName(sqlName);
            }
        }
    }

只有DruidAbstractDataSource的testConnectionInternal()方法中会调用getLastPacketReceivedTimeMs()方法

testConnectionInternal()是用来检测连接是否有效的,在获取连接和归还连接时都有可能会调用该方法,这取决于Druid检测连接是否有效的参数

Druid检测连接是否有效的参数:

  • testOnBorrow:每次获取连接时执行validationQuery检测连接是否有效(会影响性能)
  • testOnReturn:每次归还连接时执行validationQuery检测连接是否有效(会影响性能)
  • testWhileIdle:申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
  • 应用中设置了testOnBorrow=true,每次获取连接时,都会去抢占synchronized锁,所以性能下降的很明显

解决方案

经验证,使用Druid 1.x版本<=1.1.22会出现该bug,解决方案就是升级至Druid 1.x版本>=1.1.23或者Druid 1.2.x版本

GitHub issue:https://github.com/alibaba/druid/issues/3808

到此这篇关于低版本Druid连接池+MySQL驱动8.0导致线程阻塞、性能受限的文章就介绍到这了,更多相关MySQL驱动8.0低版本Druid连接池内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

MySQL 相关文章推荐
MySQL kill不掉线程的原因
May 07 MySQL
解读MySQL的客户端和服务端协议
May 10 MySQL
mysql数据库入门第一步之创建表
May 14 MySQL
MySQL注入基础练习
May 30 MySQL
MySQL七大JOIN的具体使用
Feb 28 MySQL
MySQL为数据表建立索引的原则详解
Mar 03 MySQL
面试提问mysql一张表到底能存多少数据
Mar 13 MySQL
Mysql 8.x 创建用户以及授予权限的操作记录
Apr 18 MySQL
Mysql 数据库中的 redo log 和 binlog 写入策略
Apr 26 MySQL
MySQL 数据 data 基本操作
May 04 MySQL
mysql拆分字符串作为查询条件的示例代码
Jul 07 MySQL
MySQL下载安装配置详细教程 附下载资源
Sep 23 MySQL
MySQL 8.0 驱动与阿里druid版本兼容问题解决
MySQL query_cache_type 参数与使用详解
Jul 01 #MySQL
mysql 数据插入优化方法之concurrent_insert
Jul 01 #MySQL
MySQL的Query Cache图文详解
MySQL高速缓存启动方法及参数详解(query_cache_size)
Jul 01 #MySQL
mysql优化之query_cache_limit参数说明
Jul 01 #MySQL
MySQL中存储时间的最佳实践指南
Jul 01 #MySQL
You might like
PHP获取163、gmail、126等邮箱联系人地址【已测试2009.10.10】
2009/10/11 PHP
PHP类型约束用法示例
2016/09/28 PHP
PHP 返回数组后处理方法(开户成功后弹窗提示)
2017/07/03 PHP
JavaScript 实现??打印?理
2007/04/28 Javascript
JSON格式的键盘编码对照表
2015/01/29 Javascript
基于jquery实现页面滚动到底自动加载数据的功能
2015/12/19 Javascript
JavaScript仿支付宝密码输入框
2015/12/29 Javascript
基于jQuery的网页影音播放器jPlayer的基本使用教程
2016/03/08 Javascript
JS简单测试循环运行时间的方法
2016/09/04 Javascript
jQuery用FormData实现文件上传的方法
2016/11/21 Javascript
JavaScript调用模式与this关键字绑定的关系
2018/04/21 Javascript
vue实现员工信息录入功能
2020/06/11 Javascript
nginx部署多个vue项目的方法示例
2020/09/06 Javascript
[05:49]2014DOTA2TI4正赛第二日综述 昔日冠军纷纷落马 VG LGD占尽先机
2014/07/20 DOTA
Python字符串详细介绍
2015/05/09 Python
Python增量循环删除MySQL表数据的方法
2016/09/23 Python
Python 多核并行计算的示例代码
2017/11/07 Python
[原创]Python入门教程3. 列表基本操作【定义、运算、常用函数】
2018/10/30 Python
PyCharm配置mongo插件的方法
2018/11/30 Python
PyQt5实现QLineEdit添加clicked信号的方法
2019/06/25 Python
python文档字符串(函数使用说明)使用详解
2019/07/30 Python
来自世界各地的优质葡萄酒:VineShop24
2018/07/09 全球购物
澳大利亚网上买书:Angus & Robertson
2019/07/21 全球购物
英国最受信任的在线眼镜商之一:Fashion Eyewear
2019/10/31 全球购物
咖啡书吧创业计划书
2014/01/13 职场文书
年度考核评语
2014/01/19 职场文书
美容院营销方案
2014/03/05 职场文书
春节超市活动方案
2014/08/14 职场文书
2014年最新党员对照检查材料汇总
2014/09/15 职场文书
教师批评与自我批评范文
2014/10/15 职场文书
2014财产信托协议书范本
2014/11/18 职场文书
教师个人年度总结
2015/02/11 职场文书
同意落户证明
2015/06/19 职场文书
mysql知识点整理
2021/04/05 MySQL
SQL Server远程连接的设置步骤(图文)
2022/03/23 SQL Server
Windows Server 2008 修改远程登录端口以及配置防火墙
2022/04/28 Servers