Java實現動態增加和切換數據源以訪問不同的數據庫


        有時候我們需要把數據存放到多個數據庫中,但是一個數據源只能訪問一個數據庫。想訪問不同的數據庫,那么就需要切換不同的數據源。有時候我們要切換的數據源是未知的,在程序運行的過程中才能知道要訪問哪一個數據庫,這時候就需要使用動態增加數據源的方法。我們可以先在配置文件中配置一個默認數據源,程序運行過程中需要訪問其它數據庫的時候,就動態的創建新的數據源織入到程序當中,讓程序使用該新建的數據源。將這些新建的數據源緩存起來,后面需要用到就可以獲取到,實現切換不同的數據源。主要的步驟為:“首先在配置文件中配置一個默認數據源”、“新建DynamicDataSource類來實現切換不同的數據源”、“DynamicDataSource實現動態增加數據源”、“使用Spring的AOP,將線程ThreadLocal的dbName變量清空,防止影響下一次訪問數據庫”、“在我們的產品中,不同的公司對應着不同的數據庫dbName,所以可以根據dbName創建不同的數據源”。

1、    首先在配置文件中配置一個默認數據源。
在ApplicationContext.xml中配置數據源。
(1)    配置sqlSessionFactory,指定數據源為dynamicDataSource:

	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dynamicDataSource" />
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
	</bean>

(2)    配置dynamicDataSource,為自定義的DynamicDataSource類,負責切換數據源和動態增加數據源,指定了默認數據源為jdbcDataSource_nbr_bx:

	<!--動態數據源的配置 -->
	<bean id="dynamicDataSource" class="com.bx.erp.action.interceptor.DynamicDataSource" primary="true">
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<entry value-ref="jdbcDataSource_nbr_bx" key="jdbcDataSource_nbr_bx" />
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="jdbcDataSource_nbr_bx" />
	</bean>

(3)    配置默認數據源jdbcDataSource_nbr_bx:

	<bean id="jdbcDataSource_nbr_bx" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName">
			<value>${driverClassName}</value>
		</property>
		<property name="url">
			<value>jdbc:mysql://localhost:3306/nbr_bx?useUnicode=true&amp;serverTimezone=Asia/Shanghai&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=CONVERT_TO_NULL</value>
		</property>
		<property name="username">
			<value>${db.nbx.mysql.username}</value>
		</property>
		<property name="password">
			<value>${db.nbx.mysql.password}</value>
		</property>
	</bean>

2、    新建DynamicDataSource類來實現切換不同的數據源。
Spring框架有一個AbstractRoutingDataSource類,它可以在程序運行的時候,把某個數據源動態織入到程序中,以便訪問不同的數據庫。它有一個determineCurrentLookupKey方法,該方法返回一個鍵key,通過這個key,在數據源集合中獲取其中一個數據源。最后根據獲取到的數據源去訪問數據庫。所以DynamicDataSource類可以繼承AbstractRoutingDataSource,實現determineCurrentLookupKey方法,通過返回不同的key實現切換不同的數據源,默認使用配置文件中的數據源。

public class DynamicDataSource extends AbstractRoutingDataSource {
	@Override
	protected Object determineCurrentLookupKey() {
		String dbName = DataSourceContextHolder.getDbName();

		if (dbName == null) {
			dbName = DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx; // 默認使用公共DB:nbr_bx
		} else {
			this.selectDataSource(dbName);
			if (dbName.equals(DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx)) {
				dbName = DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx;
			}
		}
		logger.debug("--------> using datasource " + dbName);

		return dbName;
	}
……

3、    DynamicDataSource實現動態增加數據源。
我們在配置文件中只配置了一個默認數據源,那么需要增加數據源,才能夠切換不同的數據源訪問不同的數據庫。
增加一個數據源的步驟如下:
例如我們要訪問一個名稱為nbr_test的數據庫,這個訪問操作對應着某一個線程,線程要訪問哪一個數據庫,我們可以使用ThreadLocal類來設置和獲取。ThreadLocal類可以防止一個線程的變量被其它線程修改。
我們用DataSourceContextHolder封裝ThreadLocal:

public class DataSourceContextHolder{
	public static final String DATASOURCE_jdbcDataSource_nbr_bx = "jdbcDataSource_nbr_bx";

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    
    public static void setDbName(String dbName) {
        contextHolder.set(dbName);
    }
    public static String getDbName() {
        return ((String) contextHolder.get());
    }
    public static void clearDbName() {
        contextHolder.remove();
    }
  
}

在訪問數據庫前,我們設置訪問nbr_test這個數據庫:

DataSourceContextHolder.setDbName(dbName);

DynamicDataSource的determineCurrentLookupKey方法獲取到當前線程的數據庫名稱dbName:

	String dbName = DataSourceContextHolder.getDbName();

DynamicDataSource的selectDataSource方法從 targetDataSources這個hashmap中查找是否存在這個數據源,因為現在還沒有這個數據源,所以需要去創建,后面創建好了之后,也會把創建好的數據源存到這個targetDataSources中:

/** @describe 數據源存在時不做處理,不存在時創建新的數據源鏈接,並將新數據鏈接添加至緩存 */
	public void selectDataSource(String dbName) {
		String sid = DataSourceContextHolder.getDbName();
		if (DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx.equals(dbName)) {
			DataSourceContextHolder.setDbName(DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx);
			return;
		}

		Object obj = this._targetDataSources.get(dbName);
		if (obj != null && sid.equals(dbName)) {
			return;
		} else {
			BasicDataSource dataSource = this.getDataSource(dbName);
			if (null != dataSource)
				this.setDataSource(dbName, dataSource);
		}
	}

     創建一個數據源:

	public BasicDataSource getDataSource(String serverId) {
		return (BasicDataSource) createDataSource(serverId);
	}

	/** 該方法為同步方法,防止並發創建兩個相同的數據庫 使用雙檢鎖的方式,防止並發 */
	private synchronized DataSource createDataSource(String dbName) {
		String url = String.format(DB_MYSQL_URL, dbName);
		//
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName(DRIVER_CLASS_NAME);
		dataSource.setUrl(url);
		dataSource.setUsername(DB_MYSQL_USERNAME);
		dataSource.setPassword(DB_MYSQL_PASSWORD);
		dataSource.setTestWhileIdle(true);
		dataSource.setValidationQuery("select 1");
		dataSource.setTestOnBorrow(true);

		return dataSource;
	}

       數據庫連接信息放在了一個配置文件中,通過注解@Value來獲取,訪問不同的的數據庫只是dbName這個字段不同,所以可以使用String.format(DB_MYSQL_URL, dbName)來修改連接url,訪問不同的數據庫。

       注解:

@Value("${driverClassName}")
	private String DRIVER_CLASS_NAME;

	@Value("${db.mysql.url}")
	private String DB_MYSQL_URL;

	@Value("${db.mysql.username}")
	private String DB_MYSQL_USERNAME;

	@Value("${db.mysql.password}")
	private String DB_MYSQL_PASSWORD;

配置文件:

# 數據庫驅動
driverClassName=com.mysql.cj.jdbc.Driver
# 數據庫URL
db.mysql.url=jdbc:mysql://localhost:3306/%s?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL
# 數據庫用戶名
db.mysql.username.encryption=81A3FD464E18C4497A79CE7CC9D5B660
db.nbx.mysql.username.encryption=81A3FD464E18C4497A79CE7CC9D5B660
# 密碼
db.mysql.password.encryption=16CCEF25E22FA42E89821B7B27858DE26DD8BFF1139FF2C70BECCA88E373F809
db.nbx.mysql.password.encryption=16CCEF25E22FA42E89821B7B27858DE26DD8BFF1139FF2C70BECCA88E373F809

     創建好一個數據源后,把它放到集合緩存中,下次就不用重新創建了,同時告訴父類AbstractRoutingDataSource,要使用當前這個數據源訪問數據庫:

 

	BasicDataSource dataSource = this.getDataSource(dbName);
		if (null != dataSource)
			this.setDataSource(dbName, dataSource);

			
	public void setDataSource(String serverId, BasicDataSource dataSource) {
		this.addTargetDataSource(serverId, dataSource);
		DataSourceContextHolder.setDbName(serverId);
	}

	public void addTargetDataSource(String key, BasicDataSource dataSource) {
		this._targetDataSources.put(key, dataSource);
		this.setTargetDataSources(this._targetDataSources);
	}

	public void setTargetDataSources(Map<Object, Object> targetDataSources) {
		this._targetDataSources = targetDataSources;
		super.setTargetDataSources(this._targetDataSources);
		afterPropertiesSet();
	}

4、    使用Spring的AOP,將線程ThreadLocal的dbName變量清空,防止影響下一次訪問數據庫。

@Aspect // for aop
@Component // for auto scan
@Order(0) // 在事務前執行
public class DataSourceInterceptor {
	@Pointcut("execution(public * com.bx.erp.action.bo.*.*(..))")
	public void dataSourceSlave() {
	};

	@Before("dataSourceSlave()")
	public void before(JoinPoint jp) {
//		System.out.println("進入切面");
	}

	@After("dataSourceSlave()")
	public void removeDataSoruce(JoinPoint joinPoint) throws Throwable {
		DataSourceContextHolder.clearDbName();
	}

5、    在我們的產品中,不同的公司對應着不同的數據庫dbName,所以可以根據dbName創建不同的數據源。

Company company = getCompanyFromSession(session);
	String dbName = company.getDbName();

	DataSourceContextHolder.setDbName(dbName);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM