spring + hibernate + 双数据源 动态(或手动)切换

8 篇文章 0 订阅

1. 定时检查数据库连接是否可用(手动切换)

​ 昨天生产库一台装有数据库的服务器关机了。虽然另一台服务器上还有服务,但是共用的一个数据库。所以。。。主服务器挂了另一台也没啥用。

​ 所有为避免再次出现这种情况,决定配置双数据库。我的需求是主数据库服务挂掉之后,副数据库能随时顶上。废话不多说,直接上代码

1. applicationContext-hibernate.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
	   xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:task="http://www.springframework.org/schema/task"
	   xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
       http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.1.xsd">
	<context:component-scan base-package="cn.com.itsea.insurance">
		<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
	</context:component-scan>

	<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="fileEncoding" value="UTF-8"/>
		<property name="locations">
			<list>
				<value>classpath:jdbc.properties</value>
			</list>
		</property>
	</bean>
    <!-- 主数据库配置 -->
	<bean id="primary_dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
		<property name="url" value="${primary.jdbc.url}" />
		<property name="driverClassName" value="${primary.jdbc.driverClassName}" />
		<property name="username" value="${primary.jdbc.username}" />
		<property name="password" value="${primary.jdbc.password}" />
		<property name="initialSize" value="20" />
		<property name="minIdle" value="20" />
		<property name="maxActive" value="200" />
		<property name="maxWait" value="60000" />
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		<property name="minEvictableIdleTimeMillis" value="300000" />
		<property name="validationQuery" value="SELECT 1 FROM DUAL" />
		<property name="testWhileIdle" value="true" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />
		<property name="poolPreparedStatements" value="true" />
		<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
		<property name="filters" value="stat" />
	</bean>
    
	<!-- 副数据库配置 -->	
	<bean id="second_dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
		<property name="url" value="${second.jdbc.url}" />
		<property name="driverClassName" value="${second.jdbc.driverClassName}" />
		<property name="username" value="${second.jdbc.username}" />
		<property name="password" value="${second.jdbc.password}" />
		<property name="initialSize" value="20" />
		<property name="minIdle" value="20" />
		<property name="maxActive" value="200" />
		<property name="maxWait" value="60000" />
		<property name="timeBetweenEvictionRunsMillis" value="60000" />
		<property name="minEvictableIdleTimeMillis" value="300000" />
		<property name="validationQuery" value="SELECT 1 FROM DUAL" />
		<property name="testWhileIdle" value="true" />
		<property name="testOnBorrow" value="false" />
		<property name="testOnReturn" value="false" />
		<property name="poolPreparedStatements" value="true" />
		<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
		<property name="filters" value="stat" />
	</bean>

	<!-- 编写spring 配置文件的配置多数源映射关系 -->
	<bean id="dataSource" class="cn.com.itsea.insurance.config.DynamicDataSourceConfig">
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<entry key="primary_dataSource" value-ref="primary_dataSource"></entry>
				<entry key="second_dataSource" value-ref="second_dataSource"></entry>
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="primary_dataSource"></property>
	</bean>

	<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="packagesToScan">
			<list>
				<value>cn.com.itsea.insurance.model</value>
			</list>
		</property>
		<property name="hibernateProperties">
			<props>
				<prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop>
				<prop key="hibernate.show_sql">false</prop>
				<prop key="hibernate.format_sql">false</prop>
			</props>
		</property>
	</bean>


	<aop:aspectj-autoproxy expose-proxy="true"/>

	<tx:annotation-driven proxy-target-class="true" transaction-manager="txManager"/>

	<bean id="txManager"class="org.springframework.orm.hibernate4.HibernateTransactionManager">
		<property name="sessionFactory" ref="sessionFactory"/>
	</bean>

	<tx:advice id="txAdvice" transaction-manager="txManager">
		<tx:attributes>
			<tx:method name="save*" propagation="REQUIRED" />
			<tx:method name="add*" propagation="REQUIRED" />
			<tx:method name="create*" propagation="REQUIRED" />
			<tx:method name="insert*" propagation="REQUIRED" />
			<tx:method name="update*" propagation="REQUIRED" />
			<tx:method name="merge*" propagation="REQUIRED" />
			<tx:method name="del*" propagation="REQUIRED" />
			<tx:method name="remove*" propagation="REQUIRED" />
			<tx:method name="put*" propagation="REQUIRED" />
			<tx:method name="use*" propagation="REQUIRED"/>
			<!--hibernate4必须配置为开启事务 否则 getCurrentSession()获取不到-->
			<tx:method name="get*" propagation="REQUIRED" read-only="true" />
			<tx:method name="count*" propagation="REQUIRED" read-only="true" />
			<tx:method name="find*" propagation="REQUIRED" read-only="true" />
			<tx:method name="list*" propagation="REQUIRED" read-only="true" />
			<tx:method name="*" read-only="true" />
		</tx:attributes>
	</tx:advice>
	<aop:config expose-proxy="true">
		<!-- 只对业务逻辑层实施事务 -->
		<aop:pointcut id="txPointcut" expression="execution(* cn.com.itsea.insurance..service..*.*(..))" />
		<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
	</aop:config>
</beans>

2. jdbc.xml
# Properties file with JDBC-related settings.
# Applied by PropertyPlaceholderConfigurer from "dataAccessContext-local.xml".
# Targeted at system administrators, to avoid touching the context XML files.

#Hangzhou
primary.jdbc.driverClassName=oracle.jdbc.OracleDriver
primary.jdbc.url=jdbc:oracle:thin:@192.168.1.2:1521:RLSBDB
primary.jdbc.username=root
primary.jdbc.password=root

second.jdbc.driverClassName=oracle.jdbc.OracleDriver
second.jdbc.url=jdbc:oracle:thin:@192.168.1.3:1521:RLSBDB
second.jdbc.username=root
second.jdbc.password=root
3. DynamicDataSourceConfig.java
package cn.com.itsea.insurance.config;


import cn.com.itsea.insurance.utils.LogUtils;
import cn.hutool.core.util.StrUtil;
import com.google.common.base.Stopwatch;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
public class DynamicDataSourceConfig extends AbstractRoutingDataSource implements ApplicationContextAware {

    private static final String PRIMARY_DATASOURCE = "primary_dataSource";
    private static final String SECOND_DATASOURCE = "second_dataSource";
    private static String USER_DATASOURCE = PRIMARY_DATASOURCE;
    private static final long SLEEP_TIME = 1000 * 60;

    private static final Lock lock = new ReentrantLock();
    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return USER_DATASOURCE;
    }

    public void initialContext(){
        new Thread(()->{
            while (true){
                try {
                    Thread.sleep(SLEEP_TIME);
                    dynamicallySwitchDataSources();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * 监听当前使用数据源是否可用
     * 如果不可用切换数据源
     * 默认为主数据源
     */
    private void dynamicallySwitchDataSources() {
        // 是否需要切换数据源
        boolean isItSwitchDataSources = false;
        Stopwatch stopwatch = Stopwatch.createStarted();
        StringBuilder logUtil = new StringBuilder("dataSources connection check");
        lock.lock();
        try {
            // 当前使用的数据源类型
            String userDataSourceType = (String) determineCurrentLookupKey();
            logUtil.append(StrUtil.format("userDataSourceType={}",userDataSourceType));
            boolean primaryDatasource = dataSourceIsClosed(PRIMARY_DATASOURCE);
            boolean secondDatasource = dataSourceIsClosed(SECOND_DATASOURCE);
            // 主数据源未关闭且正在使用副数据源
            if(!primaryDatasource && !USER_DATASOURCE.equals(PRIMARY_DATASOURCE)){
                USER_DATASOURCE = PRIMARY_DATASOURCE;
                logUtil.append(StrUtil.format("userDataSourceType Switch to {}",userDataSourceType));
                return;
            }
            // 主数据源已关闭且副数据源保持连接
            if(primaryDatasource && !secondDatasource && !USER_DATASOURCE.equals(SECOND_DATASOURCE)){
                USER_DATASOURCE = SECOND_DATASOURCE;
                logUtil.append(StrUtil.format("userDataSourceType Switch to {}",userDataSourceType));
                return;
            }
        }catch (Exception e){
            e.printStackTrace();
            logUtil.append(LogUtils.getTrace(e));
        }finally {
            // 日志处理
            logUtil.append(StrUtil.format("cost.time={}",stopwatch.elapsed(TimeUnit.MILLISECONDS)));
            log.info(logUtil.toString());
            lock.unlock();
        }
    }

    /**
     * 数据源是否关闭
     * @param dataSourceType DataSource 类型
     * @return
     */
    private boolean dataSourceIsClosed(String dataSourceType){
        DataSource dataSource = (DataSource) applicationContext.getBean(dataSourceType);
        if(null == dataSource){
            return true;
        }
        try {
            Connection connection = dataSource.getConnection();
            boolean isClosed = connection.isClosed();
            if(isClosed){
                return true;
            }
        } catch (SQLException e) {
            e.printStackTrace();
            log.error(StrUtil.format("dataSourceType={} connection closed",dataSourceType));
            return true;
        }
        return false;
    }
}

4. SpringContextHolder.java
package cn.com.itsea.insurance.utils;

import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

/**
 * 以静态变量保存Spring ApplicationContext, 可在任何代码任何地方任何时候取出ApplicaitonContext.
 * @author <a href="mailto:912349441@qq.com">tangzx</a>
 * @version V1.0
 * @ClassName: SpringContextHolder
 * @date 创建时间:2019-12-2 10:37:44
 * @since JDK 1.7
 */
@Service
@Lazy(false)
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {

	private static ApplicationContext applicationContext = null;

	private static Logger logger = LoggerFactory.getLogger(SpringContextHolder.class);

	/**
	 * 取得存储在静态变量中的ApplicationContext.
	 */
	public static ApplicationContext getApplicationContext() {
		assertContextInjected();
		return applicationContext;
	}

	/**
	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T getBean(String name) {
		assertContextInjected();
		return (T) applicationContext.getBean(name);
	}

	/**
	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
	 */
	public static <T> T getBean(Class<T> requiredType) {
		assertContextInjected();
		return applicationContext.getBean(requiredType);
	}

	/**
	 * 清除SpringContextHolder中的ApplicationContext为Null.
	 */
	public static void clearHolder() {
		if (logger.isDebugEnabled()) {
			logger.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
		}
		applicationContext = null;
	}

	/**
	 * 实现ApplicationContextAware接口, 注入Context到静态变量中.
	 */
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) {
		SpringContextHolder.applicationContext = applicationContext;
	}

	/**
	 * 实现DisposableBean接口, 在Context关闭时清理静态变量.
	 */
	@Override
	public void destroy() throws Exception {
		SpringContextHolder.clearHolder();
	}

	/**
	 * 检查ApplicationContext不为空.
	 */
	private static void assertContextInjected() {
		Validate.validState(applicationContext != null,
				"applicaitonContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder.");
	}
}

需要在系统启动完成之后,需要调用 initialContext 方法

SpringContextHolder.getBean(DynamicDataSourceConfig.class).initialContext();

每隔一段时间获取主数据源连接心跳,如果连接不可用切换到副数据库。等到主数据库启动之后再切换回去。

2. 动态切换

2.1 DynamicDataSourceConfig
package cn.com.itsea.insurance.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSourceConfig extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDataSourceType();
    }

}

2.2 DynamicDataSourceHolder.java

package cn.com.itsea.insurance.config;

public class DynamicDataSourceHolder {

    // 线程本地环境
    private static final ThreadLocal contextHolder = new ThreadLocal();

    // 设置数据源类型
    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    // 获取数据源类型
    public static String getDataSourceType() {
        return (String) contextHolder.get();
    }

    // 清除数据源类型
    public static void clearDataSourceType() {
        contextHolder.remove();
    }

}

程序会随机访问两个数据库(建议数据库之间做了主从同步)

DynamicDataSourceHolder.setDataSourceType("second_dataSource");

转载请注明:https://blog.csdn.net/CSDN_Ty/article/details/108519274

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值