前言

        本文通过一个分页sql出发,向你真是一个分页sql的源码行走路线,方便读者在自己梳理的时候,对照理解。再次进入主题,对mysql的autoReconnect=true参数做了具体实现方面的阐述。这种阐述是不全面的,但是涵盖了所有的步骤,供作者仔细钻研做一个带入。

mysql 隔一段时间重连 mysql重连机制_mysql 隔一段时间重连

一、1条sql的路线

        我们在配置mysql url的时候在连接上需要配置这个autoReconnect=true。本篇文章就让我们跟下这个参数的具体实现逻辑,并较为深刻的理解这个参数。

        我们在写一个sql实现的时候通常在BaseMapper接口上扩展自己的接口。无论我们最终怎么扩展其都会包装成代理类MybatisMapperProxy,调用其invoke方法继续走到MybatisMapperMethod的execute方法,如图所示

mysql 隔一段时间重连 mysql重连机制_mybatis_02

图1、 MybatisMapperMethod#execute

        MybatisMapperMethod#execute实现逻辑如下(分支太多,我们按照分页查询走一遍):

        1、判断命令类型,(查询、插入,更新、删除,更新磁盘)

        2、如果是查询,判断返回结果是什么类型,如果我们是分页查询,处理参数

                2.1、将入参按照param1,param2,param3的形式再组织一次参数名称和值的对应关系,并翻入一个Map.

        3、SqlSessionTemplate#selectList(java.lang.String, java.lang.Object)做了一次转发。继续走

        4、DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)

        5、将字符串statement (com.opay.online.order.aggregation.provider.server.repository.OrderAggregationRepository.selectPage)转成MappedStatement对象。对应关系在Configuration里面的mappedStatements中。

                5.1、包装MapperMethod.SqlCommand对象。

                5.2、包装MapperMethod.MethodSignature#MethodSignature对象。

        6、将MappedStatement缓存到methodCache,其key为(BaseMapper.selectPage(com.baomidou.mybatisplus.core.metadata.IPage,com.baomidou.mybatisplus.core.conditions.Wrapper))例如。

        7、调mybatis-plus的MybatisMapperMethod的execute方法。

        8、调SqlSessionTemplate的selectList方法。

        9、调用SqlSessionInterceptor的方法。

                9.1、生成SqlSession对象

                9.2、调用DefaultSqlSession的selectList

        10、在configuration中获取statement字符串对应的MappedStatement对象。

        11、调用BaseExecutor的query方法

                11.1、将mybatis格式sql转成mysql中的sql格式。

                11.2、根据输入参数sql+参数构造CacheKey

                11.3、调用queryFromDatabase方法

        12、包装RoutingStatementHandler对象

        13、执行拦截器链,由于我们是分页查询,所以走到了PaginationInterceptor的plugin方法.生成代理类为PaginationInterceptor的动态代理对象。

        14、获取连接对象PaginationInterceptor

        15、走拦截器PaginationInterceptor的intercept方法,将分页查询条件和order by语句解析出来,并添加到sql当中。

        16、调用PreparedStatementHandler的query方法

                16.1、调用sharding里面的MasterSlavePreparedStatement#execute

        17、调用druid包的DruidPooledPreparedStatement#execute方法

                17.1、检查连接池是否关闭

                17.2、获取dataSource对象executeCountUpdater的个数。

                17.3、预处理带事务的sql

                17.4、调用ClientPreparedStatement#execute方法执行远程请求,并解析结果。        

2、重试机制。

        到了这里面我们终于看到了第三方交互方法ClientPreparedStatement#execute,今天我们的核心问题开头就提到了,如果客户端的链接由于某种原因,被服务器端断开了。这个时候客户端驱动包是怎么实现的呢?前文我们提到在配置jdbc.url的时候我们添加了一个参数叫autoReconnect=true。这个参数是如何实现断开重连这个功能的呢?我们仔细看下ClientPreparedStatement#execute调用执行sql的execSQL方法。

public <T extends Resultset> T execSQL(Query callingQuery, String query, int maxRows, NativePacketPayload packet, boolean streamResults,
            ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory, ColumnDefinition cachedMetadata, boolean isBatch) {

        long queryStartTime = this.gatherPerfMetrics.getValue() ? System.currentTimeMillis() : 0;
        int endOfQueryPacketPosition = packet != null ? packet.getPosition() : 0;

        this.lastQueryFinishedTime = 0; // we're busy!

        if (this.autoReconnect.getValue() && (getServerSession().isAutoCommit() || this.autoReconnectForPools.getValue()) && this.needsPing && !isBatch) {
            try {
                ping(false, 0);
                this.needsPing = false;

            } catch (Exception Ex) {
                invokeReconnectListeners();
            }
        }

        try {
            return packet == null
                    ? ((NativeProtocol) this.protocol).sendQueryString(callingQuery, query, this.characterEncoding.getValue(), maxRows, streamResults,
                            cachedMetadata, resultSetFactory)
                    : ((NativeProtocol) this.protocol).sendQueryPacket(callingQuery, packet, maxRows, streamResults, cachedMetadata, resultSetFactory);

        } catch (CJException sqlE) {
            if (getPropertySet().getBooleanProperty(PropertyKey.dumpQueriesOnException).getValue()) {
                String extractedSql = NativePacketPayload.extractSqlFromPacket(query, packet, endOfQueryPacketPosition,
                        getPropertySet().getIntegerProperty(PropertyKey.maxQuerySizeToLog).getValue());
                StringBuilder messageBuf = new StringBuilder(extractedSql.length() + 32);
                messageBuf.append("\n\nQuery being executed when exception was thrown:\n");
                messageBuf.append(extractedSql);
                messageBuf.append("\n\n");
                sqlE.appendMessage(messageBuf.toString());
            }

            if ((this.autoReconnect.getValue())) {
                if (sqlE instanceof CJCommunicationsException) {
                    // IO may be dirty or damaged beyond repair, force close it.
                    this.protocol.getSocketConnection().forceClose();
                }
                this.needsPing = true;
            } else if (sqlE instanceof CJCommunicationsException) {
                invokeCleanupListeners(sqlE);
            }
            throw sqlE;

        } catch (Throwable ex) {
            if (this.autoReconnect.getValue()) {
                if (ex instanceof IOException) {
                    // IO may be dirty or damaged beyond repair, force close it.
                    this.protocol.getSocketConnection().forceClose();
                } else if (ex instanceof IOException) {
                    invokeCleanupListeners(ex);
                }
                this.needsPing = true;
            }
            throw ExceptionFactory.createException(ex.getMessage(), ex, this.exceptionInterceptor);

        } finally {
            if (this.maintainTimeStats.getValue()) {
                this.lastQueryFinishedTime = System.currentTimeMillis();
            }

            if (this.gatherPerfMetrics.getValue()) {
                ((NativeProtocol) this.protocol).getMetricsHolder().registerQueryExecutionTime(System.currentTimeMillis() - queryStartTime);
            }
        }

    }

如果配置了autoReconnect=true在执行sql之前会ping下远程连接,查看这个连接是否正常,如果不正常会报异常并执行invokeReconnectListeners方法。以下方法所示。

if (this.autoReconnect.getValue() && (getServerSession().isAutoCommit() || this.autoReconnectForPools.getValue()) && this.needsPing && !isBatch) {
            try {
                ping(false, 0);
                this.needsPing = false;

            } catch (Exception Ex) {
                invokeReconnectListeners();
            }
        }

        invokeReconnectListeners会调到ConnectionImpl的handleReconnect方法

public void handleReconnect() {
        createNewIO(true);
    }

        最终调到connectWithRetries(isForReconnect);方法上isForReconnect为true。

        如果我们设置了autoReconnect值为true,则会走带重试的重连机制,具体如下:

mysql 隔一段时间重连 mysql重连机制_sql_03

图二、重连个过程

三、小结

        本文通过一个分页sql梳理mybatis组件、druild组件和mysql-connector组件具体都做了那些事情。具体的说mybatis是解析mybatis文件,将其组合成mysql-connector可执行的形式,druid是为mysql-connector赋能,提供连接池的池化功能,mysql-connector-java是连接服务器的最后一层,重试参数就是在这里发挥作用。如果设置了重试参数,会在sql执行之前ping下该连接,如果ping之后不通,则会关闭异常连接,并做一个新的连接,新连接会重新跟服务器做ping操作,保证新连接的有效性。如果服务器长时间无法对外提供服务,客户端会感知异常并报错。

mysql 隔一段时间重连 mysql重连机制_mybatis_04