同事升级了 MySQL 驱动8.0,导致应用大量超时

沙海 2021年6月24日03:58:13Java评论36字数 10006阅读33分21秒阅读模式
摘要

智能摘要

智能摘要文章源自JAVA秀-https://www.javaxiu.com/34944.html

欢迎加入我的知识星球,一起探讨架构,交流源码。MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。文章源自JAVA秀-https://www.javaxiu.com/34944.html

原文约 3571 | 图片 6 | 建议阅读 8 分钟 | 评价反馈文章源自JAVA秀-https://www.javaxiu.com/34944.html

同事升级了 MySQL 驱动8.0,导致应用大量超时

点击关注 ? Java基基 文章源自JAVA秀-https://www.javaxiu.com/34944.html

收录于话题文章源自JAVA秀-https://www.javaxiu.com/34944.html

#Java基基文章源自JAVA秀-https://www.javaxiu.com/34944.html

179个文章源自JAVA秀-https://www.javaxiu.com/34944.html

点击上方“Java基基”,选择“设为星标”文章源自JAVA秀-https://www.javaxiu.com/34944.html

做积极的人,而不是积极废人!文章源自JAVA秀-https://www.javaxiu.com/34944.html

文章源自JAVA秀-https://www.javaxiu.com/34944.html

源码精品专栏文章源自JAVA秀-https://www.javaxiu.com/34944.html

 文章源自JAVA秀-https://www.javaxiu.com/34944.html

文章源自JAVA秀-https://www.javaxiu.com/34944.html

来源:blog.csdn.net/qq_40378034文章源自JAVA秀-https://www.javaxiu.com/34944.html

现象

应用升级MySQL驱动8.0后,在并发量较高时,查看监控打点,Druid连接池拿到连接并执行SQL的时间大部分都超过200ms文章源自JAVA秀-https://www.javaxiu.com/34944.html

对系统进行压测,发现出现大量线程阻塞的情况,线程dump信息如下:文章源自JAVA秀-https://www.javaxiu.com/34944.html

"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文章源自JAVA秀-https://www.javaxiu.com/34944.html

getLastPacketReceivedTimeMs()方法实现中,如果Utils.loadClass("com.mysql.jdbc.MySQLConnection")加载不到类并抛出异常,会修改变量class_connectionImpl_Error,下次调用不会再进行加载文章源自JAVA秀-https://www.javaxiu.com/34944.html

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这个类文章源自JAVA秀-https://www.javaxiu.com/34944.html

线程dump信息中可以看到是在调用TomcatEmbeddedWebappClassLoader的loadClass()方法时,导致线程阻塞的文章源自JAVA秀-https://www.javaxiu.com/34944.html

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锁,所以会出现线程阻塞,性能下降的现象文章源自JAVA秀-https://www.javaxiu.com/34944.html

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()方法文章源自JAVA秀-https://www.javaxiu.com/34944.html

testConnectionInternal()是用来检测连接是否有效的,在获取连接和归还连接时都有可能会调用该方法,这取决于Druid检测连接是否有效的参数文章源自JAVA秀-https://www.javaxiu.com/34944.html

Druid检测连接是否有效的参数文章源自JAVA秀-https://www.javaxiu.com/34944.html

  • testOnBorrow:每次获取连接时执行validationQuery检测连接是否有效(会影响性能)文章源自JAVA秀-https://www.javaxiu.com/34944.html

  • testOnReturn:每次归还连接时执行validationQuery检测连接是否有效(会影响性能)文章源自JAVA秀-https://www.javaxiu.com/34944.html

  • testWhileIdle:申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效文章源自JAVA秀-https://www.javaxiu.com/34944.html

应用中设置了testOnBorrow=true,每次获取连接时,都会去抢占synchronized锁,所以性能下降的很明显文章源自JAVA秀-https://www.javaxiu.com/34944.html

解决方案

经验证,使用Druid 1.x版本<=1.1.22会出现该bug,解决方案就是升级至Druid 1.x版本>=1.1.23或者Druid 1.2.x版本文章源自JAVA秀-https://www.javaxiu.com/34944.html

GitHub issue:https://github.com/alibaba/druid/issues/3808文章源自JAVA秀-https://www.javaxiu.com/34944.html

- END -文章源自JAVA秀-https://www.javaxiu.com/34944.html

文章源自JAVA秀-https://www.javaxiu.com/34944.html

欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢文章源自JAVA秀-https://www.javaxiu.com/34944.html

同事升级了 MySQL 驱动8.0,导致应用大量超时文章源自JAVA秀-https://www.javaxiu.com/34944.html

已在知识星球更新源码解析如下:文章源自JAVA秀-https://www.javaxiu.com/34944.html

同事升级了 MySQL 驱动8.0,导致应用大量超时文章源自JAVA秀-https://www.javaxiu.com/34944.html

同事升级了 MySQL 驱动8.0,导致应用大量超时文章源自JAVA秀-https://www.javaxiu.com/34944.html

同事升级了 MySQL 驱动8.0,导致应用大量超时文章源自JAVA秀-https://www.javaxiu.com/34944.html

同事升级了 MySQL 驱动8.0,导致应用大量超时文章源自JAVA秀-https://www.javaxiu.com/34944.html

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。文章源自JAVA秀-https://www.javaxiu.com/34944.html

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。文章源自JAVA秀-https://www.javaxiu.com/34944.html

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。文章源自JAVA秀-https://www.javaxiu.com/34944.html

文章源自JAVA秀-https://www.javaxiu.com/34944.html

文章有帮助的话,在看,转发吧。谢谢支持哟 (*^__^*)
文章源自JAVA秀-https://www.javaxiu.com/34944.html

阅读原文文章源自JAVA秀-https://www.javaxiu.com/34944.html

继续阅读
速蛙云 - 极致体验,强烈推荐!!!购买套餐就免费送各大视频网站会员!快速稳定、独家福利社、流媒体稳定解锁!速度快,全球上网、视频、游戏加速、独立IP均支持!基础套餐性价比很高!这里不多说,我一直正在使用,推荐购买:https://www.javaxiu.com/59919.html
weinxin
资源分享QQ群
本站是JAVA秀团队的技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
沙海
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定