动态数据源切换

1,初始化动态数据源,需要动态数据源的工程才需要配置

<bean class="com.dc.city.listener.datasouce.InitDataSourceListener"></bean>

2,从数据库表中读取数据源信息,并加载到服务器

public class InitDataSourceListener {

    @Resource
    private DynamicDataSource dynamicSource;    // 动态数据源bean

    @Resource
    private ServeDataSourceService dataSourceService;    // 数据源管理service

    private Log logger = LogFactory.getLog(InitDataSourceListener.class);

    /**
     * 初始化方法
     * 读取数据库正常的数据源信息,并加载到内存中
     * @author zhongdt 2016年3月7日
     */
    @PostConstruct
    public void init() {
        // 构造查询入参对象
        ServeDataSource queryParam = new ServeDataSource();
        queryParam.setIsDeleted(0);

        //从数据库获取数据源信息列表
        List<ServeDataSource> dataSourceList = dataSourceService.allList(queryParam);
        if (dataSourceList == null || dataSourceList.isEmpty()) {
            return;
        }
        Map<String, Map<String, String>> dataSourcesMap = new HashMap<String, Map<String, String>>();
        List<String> availableSourceKeys = new ArrayList<String>();

        //遍历数据,组装数据源对象map信息
        for (ServeDataSource dbInfo : dataSourceList) {
            // 加载数据源需要的map对象
            Map<String, String> infoMap = new HashMap<String, String>();
            infoMap.put("jdbcUrl", dbInfo.getSourceUrl());
            infoMap.put("driverClassName", dbInfo.getSourceClass());
            infoMap.put("username", dbInfo.getSourceUser());
            infoMap.put("password", dbInfo.getSourcePass());
            dataSourcesMap.put(dbInfo.getId() + "", infoMap);
            availableSourceKeys.add(dbInfo.getId() + "");
        }

        try {
            //加载到动态数据源对象中
            dynamicSource.setDataSource(dataSourcesMap);
        } catch (BusinessException e) {
            logger.error("数据源加载失败:"+e.getMessage());
        }
        //启动数据源监控
        startDataSouceMonitor();

    }

    /**
     * 
     * 开启新线程进行数据源连接状态监控
     *
     * @author zhongdt 2016年5月27日
     */
    private void startDataSouceMonitor() {
        // 新建监控线程池
        DataSourceMonitorThread dataSourceMonitorThread = new DataSourceMonitorThread();
        // 启动监控线程
        new Thread(dataSourceMonitorThread).start();
    }
}

3,dataSource监控线程

@Scope("prototype") 
public class DataSourceMonitorThread implements Runnable {
    private static final Log logger = LogFactory.getLog(DataSourceMonitorThread.class);
    // 循环时间间隔
    private long sleepMillis = 60*1000;

    private static DynamicDataSource dataSource;

    public DataSourceMonitorThread() {
        super();
        dataSource = (DynamicDataSource) ApplicationContextUtils.getService("dynamicDataSource");
    }

    @Override
    public void run() {
        try {
            while (true) {
                try {
                    Thread.sleep(sleepMillis);
                } catch (InterruptedException e) {
                    logger.error("dataSourceMonitorThread sleep Interrupted!", e);
                }
                // 数据源监控进程
                dataSourceMonitor();
            }
        } catch (Exception e) {
            logger.error("dataSourceMonitorThread dead, Restarting!", e);
            new Thread((DataSourceMonitorThread) ApplicationContextUtils.getService("dataSourceMonitorThread")).start();
        }
    }

    /**
     * 目前考虑每次从各个数据源取出connection,未报错就是正常
     *
     * @author zhongdt 2016年3月21日
     * @throws SQLException
     */
    private void dataSourceMonitor() {
        List<String> keys = new ArrayList<String>();
        for (Entry<Object, Object> entry : dataSource.getTargets().entrySet()) {

            String key = entry.getKey()+"";
            //如果数据源在不可用的列表中则不进行数据库连接,原因是重复操作会锁用户
            if(dataSource.getDisableKeys().contains(key)){
                continue;
            }

            DbContextHolder.setDbType(entry.getKey() + "");

            //jdk1.7 新特性 自动关闭连接
            try( Connection conn = dataSource.getConnection()) {
                keys.add(key);
            } catch (Exception e) {
                //为了避免数据库被锁只要是有报错,就把数据源信息放到不可用列表中,不在监控,需要在前端通过手动方式查看
                dataSource.getDisableKeys().add(entry.getKey()+"");
                logger.error("checkErrorDataSource, uniqueKey =:"+entry.getKey(),e);
            }
        }
        //重新设置 可用数据源列表
        dataSource.setAvaiableKeys(keys);
    }
}

4, 动态数据源
提供动态重置添加数据源功能

public class DynamicDataSource extends AbstractRoutingDataSource {

    private static Log logger = LogFactory.getLog(DynamicDataSource.class);
    // 数据源Map对象
    private Map<Object, Object> targetDataSources = new HashMap<Object, Object>();

    // 能够正常访问的数据源列表
    private List<String> avaiableKeys = new ArrayList<String>();

    // 不可用数据源key列表,在连接不成功后保存key
    private List<String> disableKeys = new ArrayList<String>();

    public List<String> getDisableKeys() {
        return disableKeys;
    }

    public void setDisableKeys(List<String> disableKeys) {
        this.disableKeys = disableKeys;
    }

    public List<String> getAvaiableKeys() {
        return avaiableKeys;
    }

    /**
     * 获取当前需要使用的数据源unique
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbType();
    }

    /**
     * 批量设置数据源信息
     *
     * @param 数据源信息map
     * @throws Exception
     * @author zhongdt 2016年3月7日
     */
    public void setDataSource(Map<String, Map<String, String>> dataSourceInfo) throws BusinessException {

        if (dataSourceInfo == null || dataSourceInfo.isEmpty()) {
            logger.warn("数据源列为空");
            throw new BusinessException("数据源列为空");
        }
        // 清空数据源列表对象
        targetDataSources.clear();

        // 根据数据源列表重新构造数据源列表对象
        for (Map.Entry<String, Map<String, String>> entry : dataSourceInfo.entrySet()) {
            String sourceKey = entry.getKey();

            Map<String, String> property = entry.getValue();
            // 必要逻辑判断
            if (property == null || property.isEmpty()) {
                logger.warn("数据源属性为空:" + entry.getKey());
                throw new BusinessException("数据源属性为空:" + entry.getKey());
            }
            if (!property.containsKey("jdbcUrl") || StringUtils.isNullOrEmpty(property.get("jdbcUrl"))) {
                throw new BusinessException("数据源url为空:" + entry.getKey());
            }
            if (!property.containsKey("driverClassName") || StringUtils.isNullOrEmpty(property.get("driverClassName"))) {
                throw new BusinessException("数据源连接驱动为空:" + entry.getKey());
            }
            if (!property.containsKey("username") || StringUtils.isNullOrEmpty(property.get("username"))) {
                throw new BusinessException("数据源用户名为空:" + entry.getKey());
            }
            if (!property.containsKey("password") || StringUtils.isNullOrEmpty(property.get("password"))) {
                throw new BusinessException("数据源密码为空:" + entry.getKey());
            }
            String jdbcUrl = property.get("jdbcUrl").trim();
            String driverClassName = property.get("driverClassName").trim();
            String username = property.get("username").trim();
            String password = property.get("password").trim();

            // 构造数据源对象atomiksBean
            AtomikosDataSourceBean sourceBean = new AtomikosDataSourceBean();
            sourceBean.setUniqueResourceName(sourceKey);
            sourceBean.setXaDataSourceClassName(driverClassName);
            sourceBean.setMaxPoolSize(99);
            Properties xaProperties = new Properties();
            xaProperties.setProperty("URL", jdbcUrl);
            xaProperties.setProperty("user", username);
            xaProperties.setProperty("password", password);
            sourceBean.setXaProperties(xaProperties);
            // 构造数据源对象map
            this.targetDataSources.put(sourceKey, sourceBean);
        }
        // 调用父类方法重新设置数据源列表
        super.setTargetDataSources(targetDataSources);

        // 调用次方法告诉spring,需要刷新数据源列表
        afterPropertiesSet();
        // 初始化数据源状态
        initDataSourceStatus();
    }

    /**
     * 添加单个 数据源到动态数据源
     * 
     * @param key:数据源 uniquerKey
     * @param property:连接属性map对象
     * @throws Exception
     * @author zhongdt 2016年3月7日
     */
    public void addDataSource(String sourceKey, Map<String, String> property) throws BusinessException {
        // 必要的逻辑判断
        if (property == null || property.isEmpty()) {
            throw new BusinessException("数据源属性为空");
        }
        // url不能为空
        if (!property.containsKey("jdbcUrl") || property.get("jdbcUrl") == null
                || StringUtils.isNullOrEmpty(property.get("jdbcUrl"))) {
            throw new BusinessException("添加数据源失败,数据源url为空:" + sourceKey);
        }
        // 连接驱动不能为空
        if (!property.containsKey("driverClassName") || property.get("driverClassName") == null
                || StringUtils.isNullOrEmpty(property.get("driverClassName"))) {
            throw new BusinessException("添加数据源失败,数据源驱动为空:" + sourceKey);
        }
        // 登录名不能为空
        if (!property.containsKey("username") || property.get("username") == null
                || StringUtils.isNullOrEmpty(property.get("username"))) {
            throw new BusinessException("添加数据源失败,数据源用户名为空:" + sourceKey);
        }
        // 密码不能为空
        if (!property.containsKey("password") || property.get("password") == null
                || StringUtils.isNullOrEmpty(property.get("password"))) {
            throw new BusinessException("添加数据源失败,数据源密码为空:" + sourceKey);
        }
        String jdbcUrl = property.get("jdbcUrl").trim();
        String driverClassName = property.get("driverClassName").trim();
        String username = property.get("username").trim();
        String password = property.get("password").trim();

        // 组装datasource对象
        AtomikosDataSourceBean sourceBean = new AtomikosDataSourceBean();
        sourceBean.setUniqueResourceName(sourceKey);
        sourceBean.setXaDataSourceClassName(driverClassName);
        Properties xaProperties = new Properties();
        // atomisk必填属性
        xaProperties.setProperty("URL", jdbcUrl);
        xaProperties.setProperty("user", username);
        xaProperties.setProperty("password", password);
        sourceBean.setXaProperties(xaProperties);

        // 添加新的数据源到数据源列表
        this.targetDataSources.put(sourceKey, sourceBean);
        // 调用父类方法重新设置数据源列表
        super.setTargetDataSources(targetDataSources);
        // 刷新数据源信息
        afterPropertiesSet();
    }

    /**
     * 修改谋个 数据源信息,并加载到内存中
     *
     * @throws Exception
     * @author zhongdt 2016年3月7日
     * @throws BusinessException 业务异常
     */
    public void modifyDataSource(String sourceKey, Map<String, String> property) throws BusinessException {
        // 必填业务逻辑判断
        if (property == null || property.isEmpty()) {
            throw new BusinessException("修改数据源失败,数据源属性为空:" + sourceKey);
        }
        if (!property.containsKey("jdbcUrl") || property.get("jdbcUrl") == null
                || StringUtils.isNullOrEmpty(property.get("jdbcUrl"))) {
            throw new BusinessException("修改数据源失败,数据源url为空:" + sourceKey);
        }
        if (!property.containsKey("driverClassName") || property.get("driverClassName") == null
                || StringUtils.isNullOrEmpty(property.get("driverClassName"))) {
            throw new BusinessException("修改数据源失败,数据源驱动为空:" + sourceKey);
        }
        if (!property.containsKey("username") || property.get("username") == null
                || StringUtils.isNullOrEmpty(property.get("username"))) {
            throw new BusinessException("修改数据源失败,数据源用户名为空:" + sourceKey);
        }
        if (!property.containsKey("password") || property.get("password") == null
                || StringUtils.isNullOrEmpty(property.get("password"))) {
            throw new BusinessException("修改数据源失败,数据源密码为空:" + sourceKey);
        }
        String jdbcUrl = property.get("jdbcUrl").trim();
        String driverClassName = property.get("driverClassName").trim();
        String username = property.get("username").trim();
        String password = property.get("password").trim();
        // 切换数据源
        DbContextHolder.setDbType(sourceKey);
        // 将已经存在的数据源取出
        AtomikosDataSourceBean sourceBean = (AtomikosDataSourceBean) determineTargetDataSource();
        // 关闭数据源连接
        sourceBean.close();
        // 情况xa对象,如果不清空的话,数据源修改不成功
        sourceBean.setXaDataSource(null);
        sourceBean.setUniqueResourceName(sourceKey);
        sourceBean.setXaDataSourceClassName(driverClassName);
        Properties xaProperties = new Properties();
        xaProperties.setProperty("URL", jdbcUrl);
        xaProperties.setProperty("user", username);
        xaProperties.setProperty("password", password);
        sourceBean.setXaProperties(xaProperties);
        // 重新初始化数据源
        try {
            sourceBean.init();
        } catch (AtomikosSQLException e) {
            throw new BusinessException("重新加载数据源失败:"+e.getMessage());
        }
        // 添加新的数据源到数据源列表
        this.targetDataSources.put(sourceKey, sourceBean);
        // 调用父类方法重新设置数据源列表
        super.setTargetDataSources(targetDataSources);
        // 刷新数据源信息
        afterPropertiesSet();

        // 初始化修改的数据源
        initDataSourceStatus(sourceKey);
    }

    /**
     * 删除数据源
     *
     * @param key 数据源 uniqueKey
     * @throws Exception
     * @author zhongdt 2016年3月10日
     */
    public void removeDataSourceByKey(String key) throws BusinessException {
        if (key == null || StringUtils.isNullOrEmpty(key)) {
            return;
        }
        // 判断是否存在
        if (!targetDataSources.containsKey(key)) {
            throw new BusinessException("数据源不存在");
        }
        // 同时删除数据源状态列表
        this.avaiableKeys.remove(key);
        this.disableKeys.remove(key);

        targetDataSources.remove(key);
        // 重新刷新数据源
        super.setTargetDataSources(targetDataSources);
        afterPropertiesSet();
    }

    /**
     * 设置可用的数据源key列表
     * 此方法供监控线程调用
     * 
     * @param keys
     * @author zhongdt 2016年3月25日
     */
    public void setAvaiableKeys(List<String> keys) {
        this.avaiableKeys = keys;
    }

    public void removeAvaiableKey(String key) {
        if (this.avaiableKeys.contains(key)) {
            this.avaiableKeys.remove(key);
        }
    }

    public void removeDisableKey(String key) {
        if (this.disableKeys.contains(key)) {
            this.disableKeys.remove(key);
        }
    }

    /**
     * 验证数据源是否存在
     *
     * @param 数据源唯一标识
     * @return true or false
     * @author zhongdt 2016年3月8日
     */
    public boolean isExists(String key) {
        if (StringUtils.isNullOrEmpty(key)) {
            return false;
        }
        return targetDataSources.containsKey(key);
    }

    /**
     * 判断数据库是否正常
     *
     * @param key
     * @return true false
     * @author zhongdt 2016年3月21日
     */
    public boolean isValid(String key) {
        if (StringUtils.isNullOrEmpty(key)) {
            return false;
        }
        return avaiableKeys.contains(key) ? true : false;
    }

    public Map<Object, Object> getTargets() {
        return this.targetDataSources;
    }

    /**
     * 
     * 解析数据库连接失败时返回的最底层的原因
     *
     * @param e
     * @return 数据库抛出的exception
     * @author zhongdt 2016年5月19日
     */
    public  Exception getCause(Exception e) {
        Exception cause = new Exception();
        //递归取最后一层的cause
        while (e != null && (e = (Exception) e.getCause()) != null) {
            cause = e;
            continue;
        }
        return cause;
    }

    /**
     * 初始化单个数据源
     * 方法的注释
     *
     * @param sourcekey
     * @author zhongdt 2016年5月3日
     */
    private void initDataSourceStatus(String sourcekey) {
        // 删除可用 及不可用标识,重新加载
        disableKeys.remove(sourcekey);
        avaiableKeys.remove(sourcekey);
        List<String> keys = new ArrayList<String>();
        // 遍历数据源对象
        for (Entry<Object, Object> entry : this.targetDataSources.entrySet()) {

            String key = entry.getKey() + "";

            if (!sourcekey.equals(key)) {
                continue;
            }
            // 先切换一次数据源,然后再取连接,如果能取到,说明连接正常
            DbContextHolder.setDbType(entry.getKey() + "");
            try (Connection conn = getConnection()) {
                // 取出一个连接,异常时候会报错
                keys.add(key);
            } catch (Exception e) {
                // 为了避免数据库被锁
                // 只要是有报错,就把数据源信息放到不可用列表中,不在监控,需要在前端通过手动方式查看
                disableKeys.add(entry.getKey() + "");
                logger.error("获取数据源conn失败, uniqueKey =:" + entry.getKey() + ",reason="+ getCause(e).getMessage());
                return;
            }
            avaiableKeys.add(key);
        }

    }



    /**
     * 初始化所有数据源连接状态
     *
     * @author zhongdt 2016年5月6日
     */
    private void initDataSourceStatus() {
        List<String> keys = new ArrayList<String>();
        for (Entry<Object, Object> entry : this.targetDataSources.entrySet()) {

            String key = entry.getKey() + "";
            // 如果数据源在不可用的列表中则不进行数据库连接,原因是重复操作会锁用户
            if (disableKeys.contains(key)) {
                continue;
            }
            // 切换数据源
            DbContextHolder.setDbType(entry.getKey() + "");
            // 获取连接,获取成功则数据源正常,失败则不正常,失败后为了避免锁账户,不再重试
            try (Connection conn = getConnection()) {
                // 取出一个连接,异常时候会报错
                keys.add(key);
            } catch (Exception e) {
                // 为了避免数据库被锁
                // 只要是有报错,就把数据源信息放到不可用列表中,不在监控,需要在前端通过手动方式查看
                disableKeys.add(entry.getKey() + "");
                logger.error("获取数据源conn失败, uniqueKey =:" + entry.getKey() + ",reason=" + getCause(e).getMessage());

            }
        }
        // 重新设置 可用数据源列表
        setAvaiableKeys(keys);
    }

    // 数据源切换context
    public static class DbContextHolder {
        // 现成安全对象,保存当前线程中使用的数据源
        private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

        public static void setDbType(String dbType) {
            contextHolder.set(dbType);
        }

        public static String getDbType() {
            return (String) contextHolder.get();
        }

        public static void clearDbType() {
            contextHolder.remove();
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值