前言
本文通过一个分页sql出发,向你真是一个分页sql的源码行走路线,方便读者在自己梳理的时候,对照理解。再次进入主题,对mysql的autoReconnect=true参数做了具体实现方面的阐述。这种阐述是不全面的,但是涵盖了所有的步骤,供作者仔细钻研做一个带入。
一、1条sql的路线
我们在配置mysql url的时候在连接上需要配置这个autoReconnect=true。本篇文章就让我们跟下这个参数的具体实现逻辑,并较为深刻的理解这个参数。
我们在写一个sql实现的时候通常在BaseMapper接口上扩展自己的接口。无论我们最终怎么扩展其都会包装成代理类MybatisMapperProxy,调用其invoke方法继续走到MybatisMapperMethod的execute方法,如图所示
图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,则会走带重试的重连机制,具体如下:
图二、重连个过程
三、小结
本文通过一个分页sql梳理mybatis组件、druild组件和mysql-connector组件具体都做了那些事情。具体的说mybatis是解析mybatis文件,将其组合成mysql-connector可执行的形式,druid是为mysql-connector赋能,提供连接池的池化功能,mysql-connector-java是连接服务器的最后一层,重试参数就是在这里发挥作用。如果设置了重试参数,会在sql执行之前ping下该连接,如果ping之后不通,则会关闭异常连接,并做一个新的连接,新连接会重新跟服务器做ping操作,保证新连接的有效性。如果服务器长时间无法对外提供服务,客户端会感知异常并报错。