HA即HighAvailable,高可用的意思。Druid中HighAvailableDataSource的实现也许能给我们提供一种数据源降级/高可用很好的实现思路。
本文围绕com.alibaba.druid.pool.ha.HighAvailableDataSource展开。
实现(几个关键组件)
HighAvailableDataSource
理所应当的,HighAvailableDataSource实现了javax.sql.DataSource接口。
他的getConnection()方法如下:
@Override
public Connection getConnection() throws SQLException {
init();
DataSource dataSource = selector.get();
if (dataSource == null) {
LOG.warn("Can NOT obtain DataSource, return null.");
return null;
}
return dataSource.getConnection();
}
因为HighAvailableDataSource实际上对多个数据源进行封装,对数据源的认证应单独位于各个数据源内。所以,他自己本身是不支持密码的。
@Override
public Connection getConnection(String username, String password) throws SQLException {
throw new UnsupportedOperationException("Not supported by HighAvailableDataSource.");
}
其中,他的一个重要的成员属性为:
private Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<String, DataSource>();
该成员属性内维护了HADataSource中所有的数据源节点。
然后,我们关注一下他的init方法:
public void init() {
...
synchronized (this) {
...
if (dataSourceMap == null || dataSourceMap.isEmpty()) {
poolUpdater.setIntervalSeconds(poolPurgeIntervalSeconds);
poolUpdater.setAllowEmptyPool(allowEmptyPoolWhenUpdate);
poolUpdater.init();
createNodeMap();
}
if (selector == null) {
setSelector(DataSourceSelectorEnum.RANDOM.getName());
}
...
inited = true;
}
}
分别调用了poolUpdater和nodeListener的init方法,并在私有方法createNodeMap中,将poolUpdater设置为nodeListener的观察者。
PoolUpdater
PoolUpdater实现了java.util.Observer接口,妥妥的观察者模式,而PoolUpdater就是事件的接受者。
该类是为了实现当事件发生后,根据事件类型,动态的修改HADataSource中维护的数据源节点。
所以,当然要看一下他实现的update方法
@Override
public void update(Observable o, Object arg) {
...
NodeEvent[] events = (NodeEvent[]) arg;
...
for (NodeEvent e : events) {
if (e.getType() == NodeEventTypeEnum.ADD) {
addNode(e);
} else if (e.getType() == NodeEventTypeEnum.DELETE) {
deleteNode(e);
}
}
}
...
很明显了,就是根据收到的事件类型动态调整dataSourceMap中的内容。
值得注意的是,在删除节点时,并不是收到事件后马上就删,而是先添加到blacklist(黑名单)中,再由后台线程去异步删除。
嗯,这么做应该是为了防止在删除的时候该数据源还在使用。
在他的init方法中,会启动一个后台线程,专门重试/处理将要进行删除的数据源。
public void init() {
...
synchronized (this) {
...
executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
removeDataSources();
} catch (Exception e) {
LOG.error("Exception occurred while removing DataSources.", e);
}
}
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS);
}
}
NodeListener
与PoolUpdater对应,NodeListener即为观察者模式中的消息发布者。而同时,之所以叫Listener,因为他也是另外一种事件的监听者。
所以NodeListener的主要作用就是将文件/zk节点变动的事件转换为自己的NodeEvent事件发布出去,通知观察者。
目前NodeListener共有以下两个实现:
FileNodeListener
基于文件变动的实现。
在该实现内,会启动后台线程,定期解析properties文件,如果本次解析出的内容和上次有不同,则会发布对应的NodeEvent给到观察者。
/**
* Load the properties file and diff with the stored Properties.
*
* @return A List of the modification
*/
@Override
public List<NodeEvent> refresh() {
Properties originalProperties = PropertiesUtils.loadProperties(file);
List<String> nameList = PropertiesUtils.loadNameList(originalProperties, getPrefix());
Properties properties = new Properties();
for (String n : nameList) {
String url = originalProperties.getProperty(n + ".url");
String username = originalProperties.getProperty(n + ".username");
String password = originalProperties.getProperty(n + ".password");
...
}
List<NodeEvent> events = NodeEvent.getEventsByDiffProperties(getProperties(), properties);
if (events != null && !events.isEmpty()) {
LOG.info(events.size() + " different(s) detected.");
setProperties(properties);
}
return events;
}
ZookeeperNodeListener
基于ZK的实现就方便很多,直接监听指定节点下的zk事件,并作出对应操作即可。
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
...
PathChildrenCacheEvent.Type eventType = event.getType();
switch (eventType) {
case CHILD_REMOVED:
updateSingleNode(event, NodeEventTypeEnum.DELETE);
break;
case CHILD_ADDED:
updateSingleNode(event, NodeEventTypeEnum.ADD);
break;
case CONNECTION_RECONNECTED:
refreshAllNodes();
break;
default:
// CHILD_UPDATED
// INITIALIZED
// CONNECTION_LOST
// CONNECTION_SUSPENDED
LOG.info("Received a PathChildrenCacheEvent, IGNORE it: " + event);
}
...
}
DataSourceSelector
因为HighAvailableDataSource实际上是对多个数据源的封装。所以,当然要有一个选择策略啦~
数据源选择策略,该类在HighAvailableDataSource中获取数据库连接他的getConnection方法中使用。目前共有如下三个实现。
NamedDataSourceSelector
根据数据源名字选取。
RandomDataSourceSelector
随机选取。
StickyRandomDataSourceSelector
带粘性的随机选取(如果5s内重复使用的话,返回的还是上一个)。
RandomDataSourceValidateThread && RandomDataSourceRecoverThread
只看名字就可以知道,这两个类分别负责对数据源的健康性检查,和针对已经在黑名单中的数据源复活。他们都实现了Runnable接口,都是在后台启动线程执行。
数据源检查
@Override
public void run() {
while (true) {
if (selector != null) {
checkAllDataSources();
maintainBlacklist();
cleanup();
} else {
break;
}
sleepForNextValidation();
}
}
黑名单恢复
@Override
public void run() {
while (true) {
if (selector != null && selector.getBlacklist() != null
&& !selector.getBlacklist().isEmpty()) {
LOG.info(selector.getBlacklist().size() + " DataSource in blacklist.");
for (DataSource dataSource : selector.getBlacklist()) {
if (!(dataSource instanceof DruidDataSource)) {
continue;
}
tryOneDataSource((DruidDataSource) dataSource);
}
} else if (selector == null) {
break;
}
sleep();
}
}