Mysql Connector/J 源码分析(LoadBalance)

文章目录

  • 前言
  • 名词定义
  • 一、Loadbalance的逻辑结构
  • 二、异常处理机制
    • 1.构造阶段
      • 小结
    • 2.使用阶段
    • 小结
  • 三、autoReconnect选项
    • 作用分析
  • 四、何时重建连接
  • 五、实用性分析
  • 六、总结

前言

本文讨论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作为MultiHostConnectionProxy的子类重载了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 groupint 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方法一探究竟:

@Overridesynchronized void pickNewConnection() throws SQLException {....if (this.currentConnection == null) { // startupthis.currentConnection = this.balancer.pickConnection(this, Collections.unmodifiableList(this.hostList),Collections.unmodifiableMap(this.liveConnections), this.responseTimes.clone(), this.retriesAllDown);return;}....}

该方法被currentConnection 属性是否为空区分出两种调用场景。在构造动态代理连接的时候,currentConnection 属性肯定为空,根据代码,它会通过balancer的pickConnection方法获取底层连接。
我们假设balancer使用的是RandomBalanceStrategy,它的pickConnection方法就是随机的从url里挑出一个ip:port组合并建立连接,若第一次不成功将会再次尝试随机挑选ip:port组合并建立连接,在允许尝试次内成功建立连接的话,就正常返回,否则会抛出通讯异常。另外,一旦出现数据异常,也会立即抛出异常。由于篇幅关系,在此不贴出代码了。

小结

在构造动态代理连接时,如果所有的ip:port都未能成功建立连接,通讯异常会抛向调用者。如果建立连接中出现数据异常,该异常也会抛向调用者。或者说在构造阶段,调用者能够感知到数据异常以及极端情况下的通讯异常。

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重载,我们进一步观察下:

@Overridepublic synchronized Object invokeMore(Object proxy, Method method, Object[] args) throws Throwable {....Object result = null;try {result = method.invoke(this.thisAsConnection, args);....} catch (InvocationTargetException e) {dealWithInvocationException(e);} finally {....}return result;}

在方法里我们看到有捕获InvocationTargetException 的行为。因为以反射的形式调用,所以一旦有问题会以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重载了:

@Overrideboolean 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;}

如果异常属于通讯异常以及用户指定的情况一旦发生,首先更新连接,然后将异常往上抛。

小结

动态代理连接在构造阶段和使用阶段,一旦遇到任何异常,都会被调用者感知到(构造阶段遇到所有的ip:port连接失败才抛出通讯异常),这一点与failover确实不一样。在使用阶段,调用者可以马上停止当前事务过程而重新再来,这样会让事务操作变得可以掌控,也可以说事务的原子性得以保证。它的这种特性,也符合我们使用普通Connection时的编程规范(try…事务操作…catch…回滚事务…finally…关闭资源…),因此,如果我们项目一开始比较小,使用普通的Connection模式,到后来需要数据库结构升级为支持多主机的时候,我们不需要修改代码,只需要修改下url配置即可。

三、autoReconnect选项

根据官网介绍autoReconnect选项是failover模块重要选项之一,在loadbalance的描述部分只字未提,但我们通过阅读代码时会看到它的身影。本节就一起分析该选项所起到的作用。
该选项对应着autoReconnect属性,而该项属性被使用到的地方仅在于
LoadBalancedConnectionProxy#invokeMore方法:

@Overridepublic 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方法情况下,大致可以梳理出以下2个场景:
1)用户已经成功获取动态代理,在使用时由于网络原因使得url里所有的ip:port都不能成功建立连接。前文讲述过invokeMore方法遇到通讯异常时,最终会执行pickNewConnection方法并抛出通讯异常。如果url里所有的ip:port都未能成功建立连接,isClosed为true。当网络恢复正常,重新执行的操作会先尝试更新连接。
2)调用者先调用接连的abort或者abortInternal方法,然后再执行增删改查的命令。也就是说调用方可能通过调用abort或者abortInternal方法主动地更换连接,然后再操作数据。

对于前一点,当网络出现极端情况下,极大方便了开发人员,因为他们不必考虑如何解决这种情况下的重连。另外,也可以理解为是一个兜底的保障作用,确保一旦网络恢复后,仍能正常执行SQL操作。
对于第二点,可理解为给调用者提供更灵活的手段,在需要的时候调用abort或者abortInternal命令更换连接,然后再进行SQL操作。所以说它是让这个机制更完善。

所以,基于上面的分析,官网上其实可以多作介绍的。

四、何时重建连接

重建连接的操作以LoadBalancedConnectionProxy#pickNewConnection为入口。该方法会轮循url各组ip:port,只要有一组能够连接成功就算建立了与Mysql的连接,如果没有一组能够成功建立连接,设置isClosed为true。
那么该方法中哪些时间点上会被调用呢?

  1. 构造动态代理连接j时,LoadBalancedConnectionProxy的构造函数会调用该方法。
  2. 正式调用Method#invoke方法前有可能执行,请看上一节。
  3. 调用Method#invoke方法出现了通讯异常。
  4. 执行完commit或者rollback方法后。

所以,如果某个ip:port对应的底层连接能够正常工作,它就有可能不会被更换。之所以说有可能,是因为当调用者仅仅发送SQL的查询命令或者仅仅一条的更新数据的SQL命令,它们不需要关闭自动提交。更进一步说,它是以事务为单位的负载均衡。之所以以事务为单位,就是要保证事务的一致性。

五、实用性分析

动态代理连接在使用中遇到异常能够被调用方即时感知到,这使得loadbalance模式具有使用的价值。故名思义,真让人以为此模式能起到负载均衡的效果,但根据前面切换连接的分析,并不完全是那回事。它其实是遇到通讯异常或者事务提交以及回滚时,更换底层连接,让调用者继续使用动态代理发送SQL命令。总之,这里涉及到Mysql多机部署和数据一致性的问题。

  • 对于微型项目,可以采用如下结构:
    在这里插入图片描述让两台Mysql服务器互为主备。

  • 让高可用性上一个台阶,可使用Percona XtraDB:
    在这里插入图片描述它可实现集群内所有Mysql服务器数据的强一致性,但是如果成员太多,为保持数据的一致性会导致性能有所下降。

  • 使用数据库中间件和集群
    在这里插入图片描述此方法由中间件分析出SQL操作是读操作还是写操作,然后将操作分配给专门用于读的服务器或者专门用于接受写命令的服务器。此方案要求最高,但它兼顾了高可用和性能。

六、总结

Loadbalance模式具有使用价值,其特点如下:

  • 调用者在使用过程中能够感知到底层发生的异常,另外调用者只需构造一个动态代理连接即可。
  • 调用者在使用过程中遇到异常,处理方法与使用普通的Connection一样。调用者如果想从普通的Connection升级到Loadbalance模式,在应用层面只需要修改url,无需改代码。
  • 可扩展性强。用户发现数据库资源占用高,可以多安排设备,然后在url里添加更多的ip:port即可。
  • Loadbalance模式会更换底层连接,所以需要部署多台Mysql服务器。另外,当前一笔事务正常运行完毕,下一笔事务遇到通讯异常,底层连接就会被替换,下次重新执行第二笔事务将在另外一台服务器上执行,所以这里存在数据同步问题。所以使用此模式,需要掌握Mysql多机的部署知识,而且必须要将数据的一致性放在最首要的位置来考虑。
  • 高可用性。根据上一点,使用好Loadbalance模式,无形中实现了数据的高可用性。
  • 以事务为单位的负载均衡,使用场景更倾向于强调写性能的应用。

根据“四、何时重建连接”一节可知,Loadbalace模式通常在遇到通讯异常或者提交/回滚事务的时候才更换连接,它的核心目的有两个:
1. 让调用者可以重复使用动态代理连接。
2. 以事务为单位的负载均衡。

此Loadbalance与我们想象中的负载均衡不完全是同一个概念。如果用户对读的响应速度有比较高的要求,这种模式就不能胜任了。后续文章将介绍replication模式,看看它在提高读的响应速度方面有何设计良方。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部