详解JDBC的Loadbalance模式

基于依赖程序的版本信息:mysql-connector-java v8.0.17

一、认识loadbalance模式

首先回忆下jdbc协议头都有哪些,下面总结下:

表1

通过表1,可以知道在loadblance模式下允许配置多个mysql节点信息,而我们每次建连时,驱动程序就会按照配置的节点,选中一个,然后完成连接的创建,下面我们来探索下它的实现。

二、基本使用

它的基本用法跟其他模式没有区别:

代码块1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql:loadbalance://127.0.0.1:3306,127.0.0.2:3306,127.0.0.3:3306/mydb";
LoadBalancedConnection connection = (LoadBalancedConnection) DriverManager.getConnection(url, "root", "123456");
Statement statement = connection.createStatement();
ResultSet rs = null;
try {
if (statement.execute("select * from t_season")) {
rs = statement.getResultSet();
}
} finally {
if (rs != null) {
rs.close();
}
statement.close();
connection.close();
}
}

三、驱动加载流程

jdbc是如何知道我们启用了loadbalance模式的?先来了解下DriverManagergetConnection方法,注意这里的DriverManager在java.sql包内,它属于jdk自带的类,目的是扫描所有实现了java.sql.Driver的类,而我们所使用的mysql-connector-java程序就实现了Driver接口,所以很容易被DriverManager载入,下面来看它是如何完成驱动程序扫描与加载的:

代码块2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();

if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}

//说明:Reflection.getCallerClass()是个本地方法,会返回调用当前这个方法的那个类的名字(后续我们称其为callerClass)
return (getConnection(url, info, Reflection.getCallerClass()));
}

private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {

//获取callerClass的类加载器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
if (callerCL == null) {
//若找不到对应的类加载器,则默认为当前线程的类加载器
callerCL = Thread.currentThread().getContextClassLoader();
}

if (url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

//这个方法会通过SPI机制加载可以加载的实现了JDBC协议的驱动程序,我们通常用的是mysql-connector-java里的驱动类,某些连接池技术也会搞一个自己的驱动类(比如Druid的DruidDriver)
//在里面会完成Driver实现类的类加载,而驱动程序只需要在静态块里将自己的实例new出来,注册到DriverManager里即可(可以去mysql-connector-java的Driver类里确认是否有该逻辑)
//所以现在我们根本不需要跟以前写jdbc程序那样写一次Class.forName的代码,这个方法已经帮我们做了(参考图1,驱动程序已经满足SPI加载配置的条件)
ensureDriversInitialized();

// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;

for (DriverInfo aDriver : registeredDrivers) { //循环已经成功加载到的驱动实现
// If the caller does not have permission to load the driver then
// skip it.
if (isDriverAllowed(aDriver.driver, callerCL)) { //这个解释在下面对应的方法里,还是挺有意思的一个方法
try {
println(" trying " + aDriver.driver.getClass().getName());
//这里是利用驱动程序获取到一个Connection对象,后面会详细讲
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}

// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

private static boolean isDriverAllowed(Driver driver, Class<?> caller) {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
return isDriverAllowed(driver, callerCL);
}

//该方法主要用来做驱动加载,以及判断加载了驱动Driver对象的类加载器跟callerClass的类加载器是否一致,若一致才返回true,反之为false
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if (driver != null) {
Class<?> aClass = null;
try {
//这里算是给驱动类调整了类加载器,将第一次进行类加载时加载到的Class对象里的类加载器统一成callerClass的
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
//这里会再确认一次driver对象此时对应的Class是否跟被调整了类加载器的Class一致(如果不出意外,这里应该是一致的)
result = ( aClass == driver.getClass() ) ? true : false;
}

return result;
}

驱动程序的SPI支持:

图1

通过上述代码,可以确认最终是通过驱动程序Driver实现类connect方法产生的Connection对象,下面来看下驱动程序Driver里的实现:

代码块3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//代码所属类:com.mysql.cj.jdbc.NonRegisteringDriver
@Override
public java.sql.Connection connect(String url, Properties info) throws SQLException {

try {
//验证传入的url是否符合jdbc连接规范
if (!ConnectionUrl.acceptsUrl(url)) {
/*
* According to JDBC spec:
* The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. This will be common, as when the
* JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn.
*/
return null;
}

//根据传入的信息,获得一个包装了连接信息的对象
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
switch (conStr.getType()) { //这个Type是根据jdbc协议头分析出来的,应该给一个对应协议头类型的Connection实例
case SINGLE_CONNECTION: //jdbc:mysql:开头的url会命中下方逻辑
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

case LOADBALANCE_CONNECTION: //jdbc:mysql:loadbalance:开头的url会命中下方逻辑(负载均衡),也是本节要讲的重点
return LoadBalancedConnectionProxy.createProxyInstance((LoadbalanceConnectionUrl) conStr);

case FAILOVER_CONNECTION: //jdbc:mysql:开头且配置了多个节点的情况会命中下方逻辑(故障转移)
return FailoverConnectionProxy.createProxyInstance(conStr);

case REPLICATION_CONNECTION: //jdbc:mysql:replication:开头的url会命中下方逻辑(主从)
return ReplicationConnectionProxy.createProxyInstance((ReplicationConnectionUrl) conStr);

default:
return null;
}

} catch (UnsupportedConnectionStringException e) {
// when Connector/J can't handle this connection string the Driver must return null
return null;

} catch (CJException ex) {
throw ExceptionFactory.createException(UnableToConnectException.class,
Messages.getString("NonRegisteringDriver.17", new Object[] { ex.toString() }), ex);
}
}

到这里,可以看到驱动程序之所以会知道我们启用了loadbalance模式,是因为我们所配置的jdbc连接协议头,根据协议头的不同,会被路由进不同的Connection实现,然后最终将Connection对象返回给用户。

四、驱动程序对LoadBalance的支持

下面重点看下命中LOADBALANCE_CONNECTION条件的LoadBalancedConnectionProxy.createProxyInstance的内部逻辑:

代码块4
1
2
3
4
5
6
//创建LoadBalancedConnection对象,并为其加上动态代理
public static LoadBalancedConnection createProxyInstance(LoadbalanceConnectionUrl connectionUrl) throws SQLException {
LoadBalancedConnectionProxy connProxy = new LoadBalancedConnectionProxy(connectionUrl);
//返回的是一个被LoadBalancedConnectionProxy代理了的LoadBalancedConnection对象
return (LoadBalancedConnection) java.lang.reflect.Proxy.newProxyInstance(LoadBalancedConnection.class.getClassLoader(), INTERFACES_TO_PROXY, connProxy);
}

4.1:LoadBalance模式相关的类关系图

到这里为止,我们已经进入了loadblance模式内部,这里返回的是一个被代理了的LoadBalancedConnection对象,下面来梳理下它们的继承和代理关系(之后重点分析的字段和方法字体均已标红):

图2

理清关系后,来看下最主要的几个属性和方法的实现。

代码块4里直接new出了LoadBalancedConnectionProxy类,并且代理的目标类为LoadBalancedConnection,通过上图,可以知道就是最终返回给用户的那个Connection对象,意味着用户拿着这个Connection做任何操作都会触发LoadBalancedConnectionProxy的invokeMore方法(通过图中展示,其父类实现了InvocationHandler接口,其invoke会触发invokeMore方法,而invokeMore方法的实现在LoadBalancedConnectionProxy里

4.2:LoadBalancedConnectionProxy.balancer属性

这是个属性,它包装了一个Balancer对象,内部有自己的LB算法,它的初始化如下:

代码块4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//拿到指定的LB算法,不配置的话默认是random,如果你想要指定,可以在jdbc连接后面追加haLoadBalanceStrategy参数,让其等于你指定的LB算法类型即可
//LB算法类型可选值在下面的switch内部,当然,你也可以自定义,自定义的话就需要传实现类的路径给这个参数了
String strategy = props.getProperty(PropertyKey.ha_loadBalanceStrategy.getKeyName(), "random");
try {
switch (strategy) {
case "random":
this.balancer = new RandomBalanceStrategy(); //random算法的实现类,默认算法
break;
case "bestResponseTime":
this.balancer = new BestResponseTimeBalanceStrategy();
break;
case "serverAffinity":
this.balancer = new ServerAffinityStrategy(props.getProperty(PropertyKey.serverAffinityOrder.getKeyName(), null));
break;
default: //你可以按需自定义LB算法,这里是通过反射的方式初始化你给定的LB算法类的
this.balancer = (BalanceStrategy) Class.forName(strategy).newInstance();
}
} catch (Throwable t) {
throw SQLError.createSQLException(Messages.getString("InvalidLoadBalanceStrategy", new Object[] { strategy }),
MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, t, null);
}

我们只关注Random即可,它的内部实现就是简单的从LoadBalancedConnectionProxy.liveConnections里随机选一个节点,然后返回出去,为了更加清晰,不再贴代码,大致流程如下(绿色框逻辑都属于RandomBalancer本身的逻辑,除此之外,图中标注了hostList属性的数据来源):

图3

它的触发点就是在LoadBalancedConnectionProxy.pickNewConnection方法(参考下方4.3),即发生在选取节点时。

4.3:LoadBalancedConnectionProxy.pickNewConnection方法

这个方法是非常核心的功能,每次变换节点时都会触发的一个方法,下面来看下其内部逻辑:

代码块5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//pick节点核心方法,利用balancer对象刷新currentConnection的值
public synchronized void pickNewConnection() throws SQLException {
if (this.isClosed && this.closedExplicitly) {
return;
}

List<String> hostPortList = Collections.unmodifiableList(this.hostsList.stream().map(hi -> hi.getHostPortPair()).collect(Collectors.toList()));

if (this.currentConnection == null) { // startup
//如果currentConnection为空,则开始利用balancer选取节点
this.currentConnection = this.balancer.pickConnection(this, hostPortList, Collections.unmodifiableMap(this.liveConnections),
this.responseTimes.clone(), this.retriesAllDown);
return; //终止
}

if (this.currentConnection.isClosed()) {
invalidateCurrentConnection(); //若发现当前连接已经被关闭了,则抛弃这个连接
}

int pingTimeout = this.currentConnection.getPropertySet().getIntegerProperty(PropertyKey.loadBalancePingTimeout).getValue();
boolean pingBeforeReturn = this.currentConnection.getPropertySet().getBooleanProperty(PropertyKey.loadBalanceValidateConnectionOnSwapServer).getValue();

//重试逻辑,若pick不成功,则在有限的次数内重试(这个次数就是hostsList的size)
for (int hostsTried = 0, hostsToTry = this.hostsList.size(); hostsTried < hostsToTry; hostsTried++) {
ConnectionImpl newConn = null;
try {
//pick节点
newConn = (ConnectionImpl) this.balancer.pickConnection(this, hostPortList, Collections.unmodifiableMap(this.liveConnections),
this.responseTimes.clone(), this.retriesAllDown);

if (this.currentConnection != null) {
if (pingBeforeReturn) { //ping检查,检查失败会抛SQLException,下方异常处理里会把它抛弃掉
newConn.pingInternal(true, pingTimeout);
}

//同步旧currentConnection节点的属性给这个新pick出来的节点,比如read-only、auto-commit什么的
syncSessionState(this.currentConnection, newConn);
}

//刷新currentConnection的值为新pick出来的这个连接
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.
this.isClosed = true; //如果将hostsList集合pick了一遍都没有找到可用的连接,则认为pick失败,标记isClosed为true
this.closedReason = "Connection closed after inability to pick valid new connection during load-balance.";
}

4.4:MultiHostConnectionProxy.invoke方法

这是MultiHostConnectionProxyInvocationHandler接口的实现,通过图2可以知道,它的子类LoadBalancedConnectionProxy是返回给用户的LoadBalancedConnection的代理类,意味着用户利用Connection做的每一步操作,都会命中这个invoke方法的调用,下面来看下这个方法的实现:

代码块6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public synchronized Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();

//若被调用的方法是getMultiHostSafeProxy,则直接返回代理对象本身(也即是用户正在使用的那个Connection对象)
if (METHOD_GET_MULTI_HOST_SAFE_PROXY.equals(methodName)) {
return this.thisAsConnection;
}

//被调用的方法是equals所执行的逻辑
if (METHOD_EQUALS.equals(methodName)) {
// Let args[0] "unwrap" to its InvocationHandler if it is a proxy.
return args[0].equals(this);
}

//被调用的方法是hashCode所执行的逻辑
if (METHOD_HASH_CODE.equals(methodName)) {
return this.hashCode();
}

//若被调用的方法是close,则关闭并清理掉所有的连接(liveConnections.clear)
if (METHOD_CLOSE.equals(methodName)) {
doClose();
this.isClosed = true; //标记isClosed为true
this.closedReason = "Connection explicitly closed.";
this.closedExplicitly = true; //标记closedExplicitly为true,意思是说这是由用户"显式关闭"的
return null;
}

//被调用的方法是abortInternal所执行的逻辑
if (METHOD_ABORT_INTERNAL.equals(methodName)) {
doAbortInternal();
this.currentConnection.abortInternal();
this.isClosed = true;
this.closedReason = "Connection explicitly closed.";
return null;
}

//被调用的方法是abort所执行的逻辑
if (METHOD_ABORT.equals(methodName) && args.length == 1) {
doAbort((Executor) args[0]);
this.isClosed = true;
this.closedReason = "Connection explicitly closed.";
return null;
}

//被调用的方法是isClosed所执行的逻辑
if (METHOD_IS_CLOSED.equals(methodName)) {
return this.isClosed;
}

try {
//若调用的方法不是上面的任意一种,则直接触发其子类的invokeMore方法(下面分析)
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);
}
}

4.5:LoadBalancedConnectionProxy.invokeMore方法

紧接着上面的代码来看,了解下invokMore方法(根据图2可知,此方法为抽象方法,由子类实现,所以它的实现逻辑LoadBalancedConnectionProxy里)

代码块7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Override
public synchronized Object invokeMore(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();

//重连机制判断,如果当前连接状态已被关闭,这里的关闭是指currentConnection的isClosed为true,而使isClosed为true的地方为:
//① 用户手动调用Connection.close,这种被称为"显式关闭",这种关闭方式连同closedExplicitly也会被置为true
//② abort连接、ping检查失败、pickNewConnection时pick不出可用节点,都会使isClosed为true,但closedExplicitly依然为false
if (this.isClosed && !allowedOnClosedConnection(method) && method.getExceptionTypes().length > 0) { // TODO remove method.getExceptionTypes().length ?
//结合上方说的②,如果你设置了autoReconnect机制(自动重连),那么就可以在任意"非显式"close的情况下,刷新currentConnection的值,使其可用
if (this.autoReconnect && !this.closedExplicitly) {
this.currentConnection = null;
pickNewConnection(); //在自动重连开启的情况下,当你的连接被非正常关闭后,会尝试重新pick节点,确保其可用
this.isClosed = false;
this.closedReason = null;
} else { //如果没有开启重连模式,那么在isClose为true时,就直接抛出错误
String reason = "No operations allowed after connection closed.";
if (this.closedReason != null) {
reason += " " + this.closedReason;
}

for (Class<?> excls : method.getExceptionTypes()) {
if (SQLException.class.isAssignableFrom(excls)) {
throw SQLError.createSQLException(reason, MysqlErrorNumbers.SQL_STATE_CONNECTION_NOT_OPEN,
null /* no access to an interceptor here... */);
}
}
throw ExceptionFactory.createException(CJCommunicationsException.class, reason);
}
}

if (!this.inTransaction) {
this.inTransaction = true;
this.transactionStartTime = System.nanoTime();
this.transactionCount++;
}

Object result = null;

try {
//触发实际的方法逻辑
result = method.invoke(this.thisAsConnection, args);

if (result != null) {
if (result instanceof com.mysql.cj.jdbc.JdbcStatement) {
((com.mysql.cj.jdbc.JdbcStatement) result).setPingTarget(this);
}
//这里是给方法返回的对象加一层代理(跟本次重点无关,不再详述)
result = proxyIfReturnTypeIsJdbcInterface(method.getReturnType(), result);
}

} catch (InvocationTargetException e) {
dealWithInvocationException(e);

} finally {
//重点是,当发现触发的方法是commit或rollback时,会刷新一下currentConnection的值,重新pick出一个
if ("commit".equals(methodName) || "rollback".equals(methodName)) {
this.inTransaction = false;

// Update stats
String host = this.connectionsToHostsMap.get(this.currentConnection);
// avoid NPE if the connection has already been removed from connectionsToHostsMap in invalidateCurrenctConnection()
if (host != null) {
synchronized (this.responseTimes) {
Integer hostIndex = (this.hostsToListIndexMap.get(host));

if (hostIndex != null && hostIndex < this.responseTimes.length) {
//更新对应节点执行事务所花费的时间
this.responseTimes[hostIndex] = System.nanoTime() - this.transactionStartTime;
}
}
}
//刷新currentConnection的值
pickNewConnection();
}
}

return result;
}

通过这块代码可以知道,一个LoadBalance模式下的连接被创建出来后,除非是commitrollback事务,否则该Connection对象里的currentConnection永远都不会变,当然,通过上述代码看,还有一种情况是会变的,那就是当前连接坏掉,然后ping检查失败isClose被标记为true,你的配置里恰好又开启autoReconnect,这时才会重新pick新的节点。

五、猜想

通过对其代码实现的分析,可以得出如下猜想:

  1. 不配autoReconnect的情况下,只有在利用该连接对象提交回滚事务时才会pick新的节点。
  2. 配置autoReconnect的情况下,在节点坏掉后,会pick一次节点,事务提交回滚一样会pick节点。
  3. 综上,如果我要实现一个select查询也需要pick节点实现负载均衡的情况下,不可以用单例Connection,因为普通select并不会触发pick操作。
  4. 综合3,想要实现全部意义的LB,必须要使用多实例模式,这样虽然实现了我想要的LB效果,但代价是巨大的,因为每次都会建连
  5. 利用连接池可以一定程度上解决这种问题,连接池可以预先建连一堆Connection对象,这些对象如果在创建时启用jdbc LoadBalance模式,那么意味着每个连接都是随机节点

六、验证

为了验证第五节的结论,我们简单做个试验验证下。

6.1:单例模式下的Query操作

代码块8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Test {

public static void main(String[] args) throws Exception {
String url = "jdbc:mysql:loadbalance://172.22.119.38:4000,172.22.119.8:4000,172.22.119.30:4000/tidb_test";
LoadBalancedConnection connection = (LoadBalancedConnection) DriverManager.getConnection(url, "tidb_test", "lPoK3QMSWY1BhSa3WCT1IWOXYkMc3Aqd");

Test test = new Test();
for (int i = 0; i < 100; i++) { //利用同一个connection对象执行100次查询操作
try {
test.triggerQuery(connection);
} finally {
//为了保证是单例模式,这里不再close
//connection.close();
}
}
}

private void triggerQuery(LoadBalancedConnection connection) throws Exception {
Statement statement = connection.createStatement();
ResultSet rs = null;
try {
//这里打印下当前参与执行sql的连接host
System.out.println("current conn host: " + connection.getHost());
if (statement.execute("select * from t_student where id = 1")) {
rs = statement.getResultSet();
}
} finally {
if (rs != null) {
rs.close();
}
statement.close();
}
}
}

这段代码运行结果打印如下:

代码块9
1
2
3
4
5
6
7
8
current conn host: 172.22.119.30
current conn host: 172.22.119.30
current conn host: 172.22.119.30
current conn host: 172.22.119.30
current conn host: 172.22.119.30
current conn host: 172.22.119.30
current conn host: 172.22.119.30
...省略,共100条...

可以看到,如果一直用同一个Connection对象去createStatement,然后执行Query,那么节点始终是一开始pick好的那个,且永远不会变

6.2:多实例下的Query操作

这个其实根本没有测试的必要,多实例意味着每次Query前都会新建一个连接对象,新建一个意味着会pick一次,那肯定是random的,我们改下main方法的代码:

代码块10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql:loadbalance://172.22.119.38:4000,172.22.119.8:4000,172.22.119.30:4000/tidb_test";

Test test = new Test();
for (int i = 0; i < 100; i++) {
//将创建connection连接放到循环体里面,使每次传给triggerQuery方法的都是不同的Connection对象
LoadBalancedConnection connection = (LoadBalancedConnection) DriverManager.getConnection(url, "tidb_test", "lPoK3QMSWY1BhSa3WCT1IWOXYkMc3Aqd");
try {
test.triggerQuery(connection);
} finally {
//别忘了close资源
connection.close();
}
}
}

这次运行结果如下:

代码块11
1
2
3
4
5
6
7
8
9
10
current conn host: 172.22.119.8
current conn host: 172.22.119.8
current conn host: 172.22.119.8
current conn host: 172.22.119.38
current conn host: 172.22.119.30
current conn host: 172.22.119.38
current conn host: 172.22.119.8
current conn host: 172.22.119.8
current conn host: 172.22.119.38
...省略,共100条...

可以看到,已经触发了LB算法了(这是预料之中的)。

你可能会问,如果调用了close呢?会不会close不是真的close,而是触发pick呢?正常情况下肯定会这样实现的吧?其实并不会,close的逻辑是把里面所有的liveConnections清空,然后close一遍,所以下面的代码会报错:

代码块12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql:loadbalance://172.22.119.38:4000,172.22.119.8:4000,172.22.119.30:4000/tidb_test";
//仍然是单例的Connection对象
LoadBalancedConnection connection = (LoadBalancedConnection) DriverManager.getConnection(url, "tidb_test", "lPoK3QMSWY1BhSa3WCT1IWOXYkMc3Aqd");

Test test = new Test();
for (int i = 0; i < 100; i++) {
try {
test.triggerQuery(connection);
} finally {
connection.close(); //这里close掉
}
}
}

执行结果如下:

图4

是的。。它直接报错了,也就是说,在select这种语句的执行下,要么你用多实例,每次都建连,实现你心目中的LB,要么你就用单例,然后一个连接用到底。。

但是它并非一无是处,比如,你可以结合连接池来用它,这样既可以保证连接复用,也可以保证池内每个连接对象的host在最大限度上不是同一个。

6.3:单例模式下的事务操作

按照源码上的理解,理论上即便是单例开启事务并提交的时候也会切换一次host,现在将前面的测试代码改成下面这样:

代码块13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql:loadbalance://172.22.119.38:4000,172.22.119.8:4000,172.22.119.30:4000/tidb_test";
//仍然是单例的Connection对象
LoadBalancedConnection connection = (LoadBalancedConnection) DriverManager.getConnection(url, "tidb_test", "lPoK3QMSWY1BhSa3WCT1IWOXYkMc3Aqd");

Test test = new Test();
for (int i = 0; i < 100; i++) {
try {
test.triggerTransaction(connection);
} finally {
//为了保证是单例模式,这里不再close
//connection.close();
}
}
}

//开启事务的方法
private void triggerTransaction(LoadBalancedConnection connection) throws Exception {
connection.setAutoCommit(false); //关闭自动提交
Statement statement = connection.createStatement();
ResultSet rs = null;
try {
//这里打印下当前参与执行sql的连接host
System.out.println("current conn host: " + connection.getHost());
if (statement.execute("select * from t_student where id = 1")) {
rs = statement.getResultSet();
}
connection.commit(); //事务提交
} finally {
if (rs != null) {
rs.close();
}
statement.close();
}
}

打印结果如下:

代码块14
1
2
3
4
5
6
7
8
9
10
11
current conn host: 172.22.119.8
current conn host: 172.22.119.30
current conn host: 172.22.119.8
current conn host: 172.22.119.30
current conn host: 172.22.119.38
current conn host: 172.22.119.30
current conn host: 172.22.119.38
current conn host: 172.22.119.30
current conn host: 172.22.119.8
current conn host: 172.22.119.30
...省略,共100条...

这是符合预期的,因为即便是单实例,每次处理事务的节点也发生了变换。

多实例就不再试了,没有必要。

6.4:开启autoReconnect时,单例执行Query操作

按照我们的结论,这个只有在节点坏掉时才会重新pick节点,以保证可用性,那么我们现在来开启它,然后依然用单例模式操作Query,代码改写如下:

代码块15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql:loadbalance://172.22.119.38:4000,172.22.119.8:4000,172.22.119.30:4000/tidb_test?autoReconnect=true"; //带上autoReconnect参数,使其为true
//仍然是单例的Connection对象
LoadBalancedConnection connection = (LoadBalancedConnection) DriverManager.getConnection(url, "tidb_test", "lPoK3QMSWY1BhSa3WCT1IWOXYkMc3Aqd");

Test test = new Test();
for (int i = 0; i < 100; i++) {
try {
test.triggerQuery(connection);
} finally {
//为了保证是单例模式,这里不再close
//connection.close();
}
}
}

运行结果打印如下:

代码块16
1
2
3
4
5
6
7
8
current conn host: 172.22.119.8
current conn host: 172.22.119.8
current conn host: 172.22.119.8
current conn host: 172.22.119.8
current conn host: 172.22.119.8
current conn host: 172.22.119.8
current conn host: 172.22.119.8
...省略,共100条...

符合我们的预期,因为它的作用不是干这个的。

七、结论

JDBC驱动程序的LoadBalance模式,是针对每一个被新建出来的Connection对象的LB,它并非很多人第一眼看到它协议头时所理解的那种将jdbc连接里配置的所有节点视作一个整体,每次利用Connection对象做一些操作时都会pick出来一个节点使用,以达到某种意义上的负载均衡,而是每次新建Connection对象时,从那堆host里pick出来其中一个,创建对应的Connection对象,这跟我们第一眼看到它的感觉不太一样,但是实现上确实没什么太大的问题,因为单纯使用JDBC时本就不提倡Connection单例复用,若想要复用,需要结合各类连接池一起使用,通过对JDBC的LB模式的了解可以知道,结合某种连接池技术来支撑,就可以达到我们想象中的LB效果,因为池内每一个Connection对象在创建时,总会触发JDBC的LB策略。