目前项目有一个需求,项目为了安全性,使用了两台数据库服务器,同时,每台服务器上都有A\B两个网卡,A\B网之前不互通,虽然采购了双机热备软件,但是只能实现将两台服务器虚拟化成一台服务器,但是虚拟的这一个服务器还是有用两个不同网段的IP
而为了数据更加安全,就想让软件能自动切换数据库连接,即A网交换机或者网络断掉之后,能通过B网访问数据库
在网上找了很久,都只有druid主从多数据源的解决办法,且主数据源必须启动,无法解决我的需求
后来,有一个思路是监听druid的事件,但是也没找到大牛提供思路,没办法,只有自己看源码
终于在今天找到了解决办法。
先贴一个我们的网络示意图
A网 和 B网是分开的,不能相互通信
开始,我模拟了启动完成后,关闭数据库服务,再通过页面访问数据的形式来模式数据库连接异常
我们可以看到,后台报了一个create connection SQLException,跟踪源码,我进入了com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run
方法里面
原来每一次创建连接的时候,都是将我们默认注入的url取出来使用
那么思路来了,如果当主数据库连接不成功后,我们将这个url更换为备机的应该是不是就能实现了
但是,bean创建后,属性值要怎么更改,我们怎么能判断是否需要更换(是否故障)
接着看源码
回到最先外层报错的位置
当创建连接成功时,可连接失败后都会调用setFailContinuous()
他会把一个属性值更改,我们只需要判断这个属性值就知道是不是异常
当正常时,failCotinuous是false
既然时属性,那么继承DruidDataSource 注入自己重写的DataSouce是不是能够生效,就能取到这一个属性
直接贴继承的DataSouce类
package com.example.demo.configuration.druid;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* 自定义DataSource
* com.example.demo.configuration.druid
* CustomizeDruidDataSource
*
* @author LiuJingPing
* @date 2019/8/1 10:50
*/
@Slf4j
public class CustomizeDruidDataSource extends DruidDataSource {
private boolean lastInited;
private String[] urls;
public String[] getUrls() {
return urls;
}
public void setUrls(String[] urls) {
this.urls = urls;
}
@Override
public void init() throws SQLException {
lastInited = inited;
super.init();
if (!lastInited && inited) {
new Thread(new ValidateUrlTask()).start();
}
}
class ValidateUrlTask implements Runnable {
@Override
public void run() {
while (true) {
// 如果这个数据源被关闭了,就结束这个定时任务
if (isClosed()) {
break;
}
//如果这个数据源已经被初始化了,同时连接异常才进行处理
if (isInited() && isFailContinuous()) {
for (String thisUrl : urls) {
Connection connection = null;
try {
connection = DriverManager.getConnection(thisUrl, username, password);
jdbcUrl = thisUrl;
} catch (Exception ignored) {
} finally {
try {
connection.close();
} catch (Exception ignored) {
}
}
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
}
}
}
}
实现思路
查源码看到,每一次获取连接时,都会调用init()
且第一个实例化时,也会进入init()
所以判断,只有init()之前inted属性为false 之后为true,那么就表示这个DataSource是被初始化了
这个时候就开启定时任务,定时通过isFailContinuous()判断链接时候异常,当异常时,取出我们所有Url去进行链接,当找到正常的可链接Url时,直接将jdbcUrl更新
那么druid在创建链接时,就会使用这个新的地址去进行链接的创建
最后一部,只要将原来的DruidDataSouce更改为我自己的CustomizeDruidDataSource 就可以了
因为我时采用的Spring boot ,所以之前是自动装配bean
我重新增加配置类
package com.example.demo.configuration.druid;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.sql.SQLException;
/**
* com.example.demo.configuration.druid
* DruidConfig
*
* @author LiuJingPing
* @date 2019/8/1 10:57
*/
@Configuration
@Data
public class DruidConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.urls}")
private String[] urls;
@Value("${spring.datasource.druid.filters}")
private String filters;
@Value("${spring.datasource.druid.max-active}")
private int maxActive;
@Value("${spring.datasource.druid.initial-size}")
private int initialSize;
@Value("${spring.datasource.druid.min-idle}")
private int minIdle;
@Value("${spring.datasource.druid.validation-query}")
private String validationQuery;
@Value("${spring.datasource.druid.test-while-idle}")
private boolean testWhileIdle;
@Value("${spring.datasource.druid.max-wait}")
private int maxWait;
@Value("${spring.datasource.druid.time-between-eviction-runs-millis}")
private int timeBetweenEvictionRunsMillis;
@Bean
public DruidDataSource druidDataSource() throws SQLException {
CustomizeDruidDataSource dataSource = new CustomizeDruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
dataSource.setUrls(urls);
dataSource.setFilters(filters);
dataSource.setMaxActive(maxActive);
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setValidationQuery(validationQuery);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setMaxWait(maxWait);
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
return dataSource;
}
}
注意urls这个属性,这个属性是我自己在配置文件中增加的
spring:
datasource:
type: com.example.demo.configuration.druid.CustomizeDruidDataSource
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8
urls: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8 , jdbc:mysql://218.89.187.94:9606/test?characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8
第一个url是默认的url,就是程序启动时进行链接的Url
urls里面我配置了两个地址,第一个和url相同,第二个就是我们备用的链接地址
注意:因为我虽然时两个网段,但是实际上链接的是同一个数据库,账号密码是一样的,表结构也完全相同,而且两台数据库服务器也是挂载同一块磁盘阵列,所以数据也是完全相同的
写在最后
当客户给我提这个需求的时候,最开始是完全没有思路了,后面有了思路也没有解决方式,本来都要放弃了,说看下源码试一下,结果就实现了
在这里我想说,如果你有一点思路的时候,网络上又没有解决方式,不妨自己仔细看一下源码,去追踪一些主要的方法,你自己就会慢慢想通的
还有-----
这种实现虽然今天测试通过了,还不确定实际使用中会不会有什么问题
当然,如果你有更好的实现方式,不妨留言交流