低版本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 相关文章推荐
教你用eclipse连接mysql数据库
Apr 22 MySQL
MySQL安装后默认自带数据库的作用详解
Apr 27 MySQL
MySQL CHAR和VARCHAR该如何选择
May 31 MySQL
MySQL索引失效的典型案例
Jun 05 MySQL
MySQL系列之三 基础篇
Jul 02 MySQL
MySql子查询IN的执行和优化的实现
Aug 02 MySQL
将MySQL的表数据全量导入clichhouse库中
Mar 21 MySQL
MySQL库表太大怎么办? 数据库分库分表项目实践
Apr 11 MySQL
MySQL索引 高效获取数据的数据结构
May 02 MySQL
mysql数据库实现设置字段长度
Jun 10 MySQL
MySQL串行化隔离级别(间隙锁实现)
Jun 16 MySQL
MySQL解决Navicat设置默认字符串时的报错问题
Jun 16 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
java EJB 加密与解密原理的一个例子
2008/01/11 PHP
PHP网站开发中常用的8个小技巧
2015/02/13 PHP
Laravel 已登陆用户再次查看登陆页面的自动跳转设置方法
2019/09/30 PHP
Apache站点配置SSL强制跳转443
2021/03/09 Servers
js 学习笔记(三)
2009/12/29 Javascript
jquery 查找select ,并触发事件的实现代码
2011/03/30 Javascript
chrome下jq width()方法取值为0的解决方法
2014/05/26 Javascript
实现前后端数据交互方法汇总
2015/04/07 Javascript
javascript 判断两个日期之差的示例代码
2015/09/05 Javascript
原生JS实现仿淘宝网左侧商品分类菜单效果代码
2015/09/10 Javascript
js拼接html字符串的注意事项
2016/10/13 Javascript
jquery hover 不停闪动问题的解决方法(亦为stop()的使用)
2017/02/10 Javascript
vue.js移动数组位置,同时更新视图的方法
2018/03/08 Javascript
在vue项目中引入highcharts图表的方法
2019/01/21 Javascript
Webpack4 使用Babel处理ES6语法的方法示例
2019/03/07 Javascript
微信小程序按钮点击动画效果的实现
2019/09/04 Javascript
layui框架与SSM前后台交互的方法
2019/09/12 Javascript
微信小程序实现打开并下载服务器上面的pdf文件到手机
2019/09/20 Javascript
vue使用exif获取图片旋转,压缩的示例代码
2020/12/11 Vue.js
python实现在pickling的时候压缩的方法
2014/09/25 Python
Python简单实现TCP包发送十六进制数据的方法
2016/04/16 Python
Python使用OpenCV进行标定
2018/05/08 Python
python绘制热力图heatmap
2020/03/23 Python
安装好Pycharm后如何配置Python解释器简易教程
2019/06/28 Python
基于Python获取城市近7天天气预报
2019/11/26 Python
推荐8款常用的Python GUI图形界面开发框架
2020/02/23 Python
Python实现邮件发送的详细设置方法(遇到问题)
2021/01/18 Python
CSS3实现多样的边框效果
2018/05/04 HTML / CSS
简单介绍HTML5中audio标签的使用
2015/09/24 HTML / CSS
校园歌咏比赛主持词
2014/03/18 职场文书
2014年幼儿园国庆主题活动方案
2014/09/16 职场文书
国庆节演讲稿范文2014
2014/09/19 职场文书
房屋维修协议书范本
2014/09/25 职场文书
社会主义核心价值观主题教育活动总结
2015/05/07 职场文书
2015年重阳节活动主持词
2015/07/30 职场文书
laravel添加角色和模糊搜索功能的实现代码
2021/06/22 PHP