文章目录
前言
本文讨论Connector/J 的loadbalance模块。我们先观察整个模块的大概逻辑结构和每一个大组件的作用。然后在代码层面分析对于异常的控制,这里会有两个“区分”:1)区分构造连接过程和使用连接过程;2)区分通讯异常和数据异常。最终分析此模式的实用性。
本次分析的版本为5.1.46。若通过maven下载,可添加以下依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
我们获取连接的例子如下:
Connection conn = null;
URL =“jdbc:mysql:loadbalance://ip1:port1,ip2:port2,ip3:port3/dbname”;
try{
// 注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// 打开链接
conn = DriverManager.getConnection(DB_URL,USER,PASS);
....
名词定义
Mysql:Mysql数据库管理软件
Mysql服务器:安装了Mysql数据库管理软件的服务器
调用方:调用DriverManager#getConnection命令获取连接的一方
一、Loadbalance的逻辑结构
Loadbalance模块的UML类图如下:
主要组件功能如下:
- MysqlIO:负责与Mysql服务器建立tcp链接。
- ConnectionImpl、JDBC4Connection:通过MysqlIO控制与Mysql服务器间的连接,并设定和记录各种连接时间
- MultiHostMySQLConnection、LoadBalancedMySQLConnection、JDBC4LoadBalancedMySQLConnection:通过代理对象LoadBalancedConnectionProxy获取JDBC4Connection对象,并调用JDBC4Connection对象对应的接口方法(从JDBC4MySQLConnection到java.sql.Connection声明的方法),或者调用LoadBalancedConnectionProxy对应的方法。
- BalanceStrategy接口以及其实现类,以各自的算法返回合适的连接对象JDBC4Connection。
- MultiHostConnectionProxy:作为各种动态类的父类,实现了各种动态类的公共方法,最常见的就是返回当前连接对象给到MultiHostMySQLConnection及其子类。它还是InvocationHandler接口的直接实现类,它重载了invoke方法,并声明了由其子类实现的虚方法invokeMore。invoke方法的实现使用了模板方法这种设计模式。通过invoke方法和类的invokeMore方法,一起实现了代理模式,即在被代理方法执行之前和之后都添加了一些行为。
- LoadBalancedConnectionProxy作为MultiHostMySQLConnection的子类重载了invokeMore方法,在被代理方法执行后,会通过策略对象计算和更新当前连接对象,以供下次使用。
组件调用顺序:
- 构造连接时各组件调用顺序:
- 使用连接时各组件调用顺序:
二、异常处理机制
1.构造阶段
在我们的例里,url以jdbc:mysql:loadbalance:作为前缀,当我们使用DriverManager#getConnection命令获取连接的时候,跟踪调用链会一直来到NonRegisteringDriver#connect方法。而该方法分析参数url后,知道这是使用loadbalance模块,因此就会进入NonRegisteringDriver#connectLoadBalanced方法。该方法将调用LoadBalancedConnectionProxy#createProxyInstance方法获取连接。
在上述调用链的过程中,并没有捕获异常的行为。因此,如果LoadBalancedConnectionProxy#createProxyInstance方法的底层抛出的任何异常都会直接抛向调用方。所以调用方通常都会有一个获取SQLException异常的行为。
我们观察下LoadBalancedConnectionProxy#createProxyInstance方法:
public static LoadBalancedConnection createProxyInstance(List<String> hosts, Properties props) throws SQLException {
LoadBalancedConnectionProxy connProxy = new LoadBalancedConnectionProxy(hosts, props);
return (LoadBalancedConnection) java.lang.reflect.Proxy.newProxyInstance(LoadBalancedConnection.class.getClassLoader(), INTERFACES_TO_PROXY, connProxy);
}
这里只有两行命令,第一行构造LoadBalancedConnectionProxy对象,根据命令的声明,它有可能抛出SQLException;第二句是创建动态代理对象,根据命令的声明,它有可能抛出IllegalArgumentException,这异常是RuntimeException的子类。因此我们只需要着重观察LoadBalancedConnectionProxy对象的构造过程。
private LoadBalancedConnectionProxy(List<String> hosts, Properties props) throws SQLException {
super();
....
}
私有的构造函数首先会调用父类的构造函数,里面JDBC4LoadBalancedMySQLConnection对象,并作为thisAsConnection属性值。而当前的LoadBalancedConnectionProxy对象作为JDBC4LoadBalancedMySQLConnection对象的thisAsProxy属性值。在这个过程中,其实不会抛出SQLException异常。
我们再回到LoadBalancedConnectionProxy构造函数:
private LoadBalancedConnectionProxy(List<String> hosts, Properties props) throws SQLException {
super();
....
// hosts specifications may have been reset with settings from a previous connection group
int numHosts = initializeHostsSpecs(hosts, props);
this.liveConnections = new HashMap<String, ConnectionImpl>(numHosts);
this.hostsToListIndexMap = new HashMap<String, Integer>(numHosts);
for (int i = 0; i < numHosts; i++) {
this.hostsToListIndexMap.put(this.hostList.get(i), i);
}
this.connectionsToHostsMap = new HashMap<ConnectionImpl, String>(numHosts);
....
String strategy = this.localProps.getProperty("loadBalanceStrategy", "random");
if ("random".equals(strategy)) {
this.balancer = (BalanceStrategy) Util.loadExtensions(null, props, RandomBalanceStrategy.class.getName(), "InvalidLoadBalanceStrategy", null)
.get(0);
} else if ("bestResponseTime".equals(strategy)) {
this.balancer = (BalanceStrategy) Util
.loadExtensions(null, props, BestResponseTimeBalanceStrategy.class.getName(), "InvalidLoadBalanceStrategy", null).get(0);
} else if ("serverAffinity".equals(strategy)) {
this.balancer = (BalanceStrategy) Util.loadExtensions(null, props, ServerAffinityStrategy.class.getName(), "InvalidLoadBalanceStrategy", null)
.get(0);
} else {
this.balancer = (BalanceStrategy) Util.loadExtensions(null, props, strategy, "InvalidLoadBalanceStrategy", null).get(0);
}
String lbExceptionChecker = this.localProps.getProperty("loadBalanceExceptionChecker", "com.mysql.jdbc.StandardLoadBalanceExceptionChecker");
this.exceptionChecker = (LoadBalanceExceptionChecker) Util.loadExtensions(null, props, lbExceptionChecker, "InvalidLoadBalanceExceptionChecker", null)
.get(0);
....
pickNewConnection();
}
它根据url里的ip:port集合填充了hostsToListIndexMap属性,key为ip:port的值,value为从0开始的下标值。同时也构造了与ConnectionImpl有关的属性:liveConnections和connectionsToHostsMap,这两属性会在后面添加ConnectionImpl对象。
随后还会根据url里的loadBalanceStrategy选项构造对应的策略对象,并作为balance属性值。
最后初始化了异常检查器exceptionChecker,如果url里有指定的就使用指定的类来构造,没有的话就使用默认的com.mysql.jdbc.StandardLoadBalanceExceptionChecker对象。
在构造函数的方法体里,pickNewConnection方法之前,虽然有些调用的方法会抛出异常,但都有捕获行为,因此到目前为止仍然安全,我们接着进入非常眼熟的pickNewConnection方法一探究竟:
@Override
synchronized void pickNewConnection() throws SQLException {
....
for (int hostsTried = 0, hostsToTry = this.hostList.size(); hostsTried < hostsToTry; hostsTried++) {
ConnectionImpl newConn = null;
try {
newConn = this.balancer.pickConnection(this, Collections.unmodifiableList(this.hostList), Collections.unmodifiableMap(this.liveConnections),
this.responseTimes.clone(), this.retriesAllDown);
if (this.currentConnection != null) {
if (pingBeforeReturn) {
if (pingTimeout == 0) {
newConn.ping();
} else {
newConn.pingInternal(true, pingTimeout);
}
}
syncSessionState(this.currentConnection, newConn);
}
this.currentConnection = newConn;
return;
} catch (SQLException e) {
if (shouldExceptionTriggerConnectionSwitch(e) && newConn != null) {
// connection error, close up shop on current connection
invalidateConnection(newConn);
}
}
}
// no hosts available to swap connection to, close up.
....
}
该方法有一个很大的for循环,嵌套其内的是很大的一个try catch 代码块。这段代码的意思就是试图通过策略对象来获取连接对象,而最大次数为url里ip:port的数量。获取到连接对象后,还要通过ping的行为确保连接正常。如果连接被成功构造,那么就作为currentConnection属性值。因为这些行为都需要发生网络连接以及与mysql发生数据交互,所以都有可能产生异常,而根据《Mysql Connector/J 源码分析(普通Connection)》所述,构造连接过程中产生的通讯异常和数据异常都会统一以SQLException形式往外抛,因此,在此处就会被捕获。异常的处理是本节关注的内容,所以我们先观察catch部分。此处先调用shouldExceptionTriggerConnectionSwitch命令判断异常,然后再判断连接对象是否已经构造了。为什么要判断连接对象是否已经构造了呢?貌似多余,实际是安全的做法。因为上面提过,是先构造连接,然后通过连接发送ping命令,这两步是一前一后,可能在构造连接的时候网络是正常的,但发送ping命令时现状况是有可能的,所以就会出现构造了连接对象但又会抛出异常的情况。
@Override
boolean shouldExceptionTriggerConnectionSwitch(Throwable t) {
return t instanceof SQLException && this.exceptionChecker.shouldExceptionTriggerFailover((SQLException) t);
}
我们假设使用的是默认的异常检查器com.mysql.jdbc.StandardLoadBalanceExceptionChecker,该类有两个集合sqlStateList和sqlExClassList,而集合的元素分别来自于url的loadBalanceSQLStateFailover和loadBalanceSQLExceptionSubclassFailover选项,看客可以通过官网了解这两选项,在此不详述。
shouldExceptionTriggerFailover方法在以下情况此方法会返回true:
- 错误码以08开头
- 错误码虽然不以08开头,但错误码以sqlStateList任一元素值开头
- 抛出的异常是通讯异常CommunicationsException
- 抛出的异常类型是sqlExClassList任一元素的异常类型相等或者是子类。
也就是说,只要是通讯异常或者用户指定的异常,该方法都会返回true。
我们把目光拉回到LoadBalancedConnectionProxy#pickNewConnection方法的捕获异常位置:
@Override
synchronized void pickNewConnection() throws SQLException {
....
for (int hostsTried = 0, hostsToTry = this.hostList.size(); hostsTried < hostsToTry; hostsTried++) {
ConnectionImpl newConn = null;
try {
....
} catch (SQLException e) {
if (shouldExceptionTriggerConnectionSwitch(e) && newConn != null) {
// connection error, close up shop on current connection
invalidateConnection(newConn);
}
}
}
// no hosts available to swap connection to, close up.
this.isClosed = true;
this.closedReason = "Connection closed after inability to pick valid new connection during load-balance.";
}
总的来说,当一个ip:port相对应的连接构造时或者执行ping命令时,底层抛出了SQLException后都会尝试另一个ip:port。每个被尝试建立的连接哪怕抛出SQLException(通讯异常和数据异常)都会被捕获。如果每一组的ip:port都未能成功构造连接,那么currentConnection属性值为null,isClosed为true。调用者是无法正常使用连接的。
经过上面的分析,在构造连接代理LoadBalancedConnectionProxy对象时,底层抛出的SQLException异常不会传导到调用者。
2.使用阶段
MultiHostConnectionProxy直接重载了java.lang.reflect.InvocationHandler接口的invoke方法,我们先观察一下:
public synchronized Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
....
try {
return invokeMore(proxy, method, args);
} catch (InvocationTargetException e) {
throw e.getCause() != null ? e.getCause() : e;
} catch (Exception e) {
// Check if the captured exception must be wrapped by an unchecked exception.
Class<?>[] declaredException = method.getExceptionTypes();
for (Class<?> declEx : declaredException) {
if (declEx.isAssignableFrom(e.getClass())) {
throw e;
}
}
throw new IllegalStateException(e.getMessage(), e);
}
}
该方法的省略部分不会抛异常。但它对invokeMore有一个获取异常的行为,并且区分了InvocationTargetException和其他的情况的异常,从这里来看,底层抛上来的异常都会往调用者抛。而invokeMore方法被LoadBalancedConnectionProxy重载,我们进一步观察下:
@Override
public synchronized Object invokeMore(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (this.isClosed && !allowedOnClosedConnection(method) && method.getExceptionTypes().length > 0) {
if (this.autoReconnect && !this.closedExplicitly) {
// try to reconnect first!
this.currentConnection = null;
pickNewConnection();
this.isClosed = false;
this.closedReason = null;
....
Object result = null;
try {
result = method.invoke(this.thisAsConnection, args);
if (result != null) {
if (result instanceof com.mysql.jdbc.Statement) {
((com.mysql.jdbc.Statement) result).setPingTarget(this);
}
result = proxyIfReturnTypeIsJdbcInterface(method.getReturnType(), result);
}
} catch (InvocationTargetException e) {
dealWithInvocationException(e);
} finally {
....
pickNewConnection();
....
}
return result;
}
在方法里我们看到有两处调用了pickNewConnection方法。前文已经分析过,里面不会有异常抛出。那么,我们需要分析的点就集中在try catch 里了。因为以反射的形式调用,所以一旦有问题会以InvocationTargetException的形式抛出,然后进入MultiHostConnectionProxy#dealWithInvocationException方法进一步分析原始异常类型:
void dealWithInvocationException(InvocationTargetException e) throws SQLException, Throwable, InvocationTargetException {
Throwable t = e.getTargetException();
if (t != null) {
if (this.lastExceptionDealtWith != t && shouldExceptionTriggerConnectionSwitch(t)) {
invalidateCurrentConnection();
pickNewConnection();
this.lastExceptionDealtWith = t;
}
throw t;
}
throw e;
}
从代码结构上看,异常是一定会往上抛。因为MultiHostConnectionProxy#dealWithInvocationException方法是在LoadBalancedConnectionProxy#invokeMore的catch代码块里调用,而该catch代码块没有进一步捕获异常,因此异常将到达MultiHostConnectionProxy#invoke方法里的捕获异常的代码块,前文通过观察它的捕获异常代码块可知道,异常最终会继续向上层抛。也就是说,在使用连接的过程中,一旦遇到通讯异常或者数据异常,调用者都会感知到。当异常发生时,我们可以基本知道它经过的地方:
我们把目光放回到MultiHostConnectionProxy#dealWithInvocationException:
void dealWithInvocationException(InvocationTargetException e) throws SQLException, Throwable, InvocationTargetException {
Throwable t = e.getTargetException();
if (t != null) {
if (this.lastExceptionDealtWith != t && shouldExceptionTriggerConnectionSwitch(t)) {
invalidateCurrentConnection();
pickNewConnection();
this.lastExceptionDealtWith = t;
}
throw t;
}
throw e;
}
调用的shouldExceptionTriggerConnectionSwitch被LoadBalanceConnectionProxy重载了:
@Override
boolean shouldExceptionTriggerConnectionSwitch(Throwable t) {
return t instanceof SQLException && this.exceptionChecker.shouldExceptionTriggerFailover((SQLException) t);
}
我们假设exceptionChecker使用的是默认的com.mysql.jdbc.StandardLoadBalanceExceptionChecker
该类有两个集合sqlStateList和sqlExClassList,而集合的元素分别来自于url的loadBalanceSQLStateFailover和loadBalanceSQLExceptionSubclassFailover选项,看官可以通过官网了解这两选项,在此不详述。
shouldExceptionTriggerFailover方法在以下情况此方法会返回true:
- 错误码以08开头
- 错误码虽然不以08开头,但错误码以sqlStateList任一元素值开头
- 抛出的异常是通讯异常CommunicationsException
- 抛出的异常类型是sqlExClassList任一元素的异常类型相等或者是子类。
也就是说,只要是通讯异常或者用户指定的异常,该方法都会返回true。
我们再次结合MultiHostConnectionProxy#dealWithInvocationException来看:
void dealWithInvocationException(InvocationTargetException e) throws SQLException, Throwable, InvocationTargetException {
Throwable t = e.getTargetException();
if (t != null) {
if (this.lastExceptionDealtWith != t && shouldExceptionTriggerConnectionSwitch(t)) {
invalidateCurrentConnection();
pickNewConnection();
this.lastExceptionDealtWith = t;
}
throw t;
}
throw e;
}
如果异常属于通讯异常以及用户指定的情况一旦发生,首先更新连接,然后将异常往上抛。
小结
动态代理连接在构造阶段即使底层无法成功建立连接,但动态代理连接是能够成功构造并被调用者持有。而动态代理连接在使用过程中,一旦遇到任何异常,都会被调用者感知到,这一点与failover确实不一样。调用者可以马上停止当前事务过程而重新再来,这样会让事务操作变得可以掌控,也可以说事务的原子性得以保证。它的这种特性,也符合我们使用普通Connection时的编程规范(try…事务操作…catch…回滚事务…finally…关闭资源…),因此,如果我们项目一开始比较小,使用普通的Connection模式,到后来需要数据库结构升级为支持负载均衡的时候,我们不需要修改代码,只需要修改下url配置即可。
三、autoReconnect选项
根据官网介绍autoReconnect选项是failover模块重要选项之一,在loadbalance的描述部分只字未提,但我们通过阅读代码时会看到它的身影。本节就一起分析该选项所起到的作用。
该选项对应着autoReconnect属性,而该项属性被使用到的地方仅在于
LoadBalancedConnectionProxy#invokeMore方法:
@Override
public synchronized Object invokeMore(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (this.isClosed && !allowedOnClosedConnection(method) && method.getExceptionTypes().length > 0) {
if (this.autoReconnect && !this.closedExplicitly) {
// try to reconnect first!
this.currentConnection = null;
pickNewConnection();
....
Object result = null;
try {
result = method.invoke(this.thisAsConnection, args);
....
return result;
}
在Method#invke前,它是调用pickNewConnection的条件之一,我们分析下各项条件成立的因素:
- this.isClosed为真值
该值为真值的地方出现在:
1)pickNewConnection方法体里,如果url的ip:port都不能成功建立连接,就为真值。
2)doPing方法体里,如果未能ping成功,就为真值。
3)执行连接的close方法
4)执行连接的abortInternal方法
5)执行连接的abort方法 - !allowedOnClosedConnection为真值
只要不执行连接的以下方法就会成立:
1)连接的getAutoCommit方法
2)连接的getCatalog方法
3)连接的getTransactionIsolation方法
4)连接的getSessionMaxRows方法 - method.getExceptionTypes().length > 0
只要方法声明会抛异常即满足。 - this.autoReconnect
url上添加autoReconnect=true选项即满足。 - !this.closedExplicitly
不执行连接的close方法即可为真:
作用分析
所以假设用户在url添加了autoReconnect=true的情况下,并且调用方调用动态连接的上一条命令不是close方法情况下,大致可以梳理出以下3个场景:
1)当用户获取连接动态代理时会执行pickNewConnection方法,当url里所有的ip:port都不能成功建立连接时isClosed为true。因为调用方不主动调用LoadBalancedConnectionProxy#getCurrentActiveHost方法是感知不到底层连接没有建立,所以当他继续使用动态代理连接执行查询或者更新数据的SQL命令时,会先尝试更新连接。
2)用户已经成功获取动态代理,在使用时由于网络原因使得url里所有的ip:port都不能成功建立连接。前文讲述过遇到通讯异常时,会执行pickNewConnection方法并抛出通讯异常。如果url里所有的ip:port都未能成功建立连接,isClosed为true。当网络恢复正常,重新执行的操作会先尝试更新连接。
3)调用者先调用接连的abort或者abortInternal方法,然后再执行增删改查的命令。也就是说调用方可能通过调用abort或者abortInternal方法主动地更换连接,然后再操作数据。
对于前两点,当网络出现极端情况下,极大方便了开发人员,因为他们不必考虑如何解决这种情况下的重连。另外,也可以理解为是一个兜底的保障作用,确保一旦网络恢复后,仍能正常执行SQL操作。
对于第三点,可理解为给调用者提供更灵活的手段,在需要的时候调用abort或者abortInternal命令更换连接,然后再进行SQL操作。所以说它是让这个机制更完善。
所以,基于上面的分析,官网上其实可以多作介绍的。
四、何时重建连接
重建连接的操作以LoadBalancedConnectionProxy#pickNewConnection为入口。该方法会轮循url各组ip:port,只要有一组能够连接成功就算建立了与Mysql的连接,如果没有一组能够成功建立连接,设置isClosed为true。
那么该方法中哪些时间点上会被调用呢?
- 构造动态代理连接j时,LoadBalancedConnectionProxy的构造函数会调用该方法。
- 正式调用Method#invoke方法前,请看上一节。
- 调用Method#invoke方法出现了通讯异常。
- 执行完commit或者rollback方法后。
五、实用性分析
动态代理连接在使用中遇到异常能够被调用方即时感知到,这使得loadbalance模式具有使用的价值。故名思义,此模式还能起到负载均衡的效果,用户只需要在url上多配置几组ip:port(mysql的服务器得跟上), 这样就可以均衡各台数据库服务器的压力,至少系统进行极力测试时可以轻松过关_。
Loadbalance模式是依赖Strategy组件来分配任务到某一组ip:port对应的Mysql服务器上,而不并会区分任务是写操作还是读操作,所以这就要求Mysql服务器间必须具备数据同步功能,以保证各台服务器上的数据是一致的,而且需要实现强一致性,否则无法达到事务隔离的Repeatable级别的要求。
-
对于微型项目,可以采用如下结构:
让两台Mysql服务器互为高备。 -
让高可用性上一个台阶,可使用Percona XtraDB:
它可实现集群内所有Mysql服务器数据的强一致性,但是如果成员太多,为保持数据的一致性会导致性能有所下降。 -
使用数据库中间件和集群
此方法由中间件分析出SQL操作是读操作还是写操作,然后将操作分配给专门用于读的服务器或者专门用于接受写命令的服务器。此方案要求最高,但它兼顾了高可用和性能。
小结
Loadbalance模式强调的是压力的均衡,所以要使用好这种连接模式,用户还需要考虑Mysql的拓扑和数据同步问题。这样才能够符合当代应用对于数据高可用的要求。如果用户对于数据写操作的耗时不敏感,用户完全可以采用此模式。
六、总结
Loadbalance模式具有使用价值,其特点如下:
- 调用者在使用过程中能够感知到底层发生的异常,另外调用者只需构造一个动态代理连接即可。
- 调用者在使用过程中遇到异常,处理方法与使用普通的Connection一样。使用者如果想从普通的Connection升级到Loadbalance模式,在应用层面只需要修改url,无需改代码。
- 可扩展性强。用户发现数据库资源占用高,可以多安排设备,然后在url里添加更多的ip:port即可。
- Loadbalance模式强调的是压力的均衡,所以需要部署多台Mysql服务器。另外,当要执行一批的SQL 命令时,前后命令有可能会分配给不同的Mysql服务器执行,所以这里存在数据同步问题。所以使用此模式,需要掌握Mysql多机的部署知识,而且必须要将数据的一致性放在最首要的位置来考虑。
- 高可用性。根据上一点,使用好Loadbalance模式,无形中实现了数据的高可用性。
- 稳定性。因为将执行任务安排到不同的Mysql服务器上,降低各台服务器的资源占用,从而为整个应用系统的稳定运行起到了一定的作用。
Loadbalace模式以将任务分配到不同的服务器上执行作为手段,实现整个数据库系统的稳定运行,这种模式强调的是稳定性,如果用户对响应速度要求极高,这种模式就不能胜任了。后续文章将介绍replication模式,看看它在提高响应速度方面有何设计良方。