Druid中的HADataSource

1,133 阅读3分钟

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();
        }
    }