• 首页
  • 栏目
  • ERP
  • Java实现动态增加和切换数据源以访问不同的数据库

Java实现动态增加和切换数据源以访问不同的数据库

  • 2021-12-28
  • Admin

        有时候我们需要把数据存放到多个数据库中,但是一个数据源只能访问一个数据库。想访问不同的数据库,那么就需要切换不同的数据源。有时候我们要切换的数据源是未知的,在程序运行的过程中才能知道要访问哪一个数据库,这时候就需要使用动态增加数据源的方法。我们可以先在配置文件中配置一个默认数据源,程序运行过程中需要访问其它数据库的时候,就动态的创建新的数据源织入到程序当中,让程序使用该新建的数据源。将这些新建的数据源缓存起来,后面需要用到就可以获取到,实现切换不同的数据源。主要的步骤为:“首先在配置文件中配置一个默认数据源”、“新建DynamicDataSource类来实现切换不同的数据源”、“DynamicDataSource实现动态增加数据源”、“使用Spring的AOP,将线程ThreadLocal的dbName变量清空,防止影响下一次访问数据库”、“在我们的产品中,不同的公司对应着不同的数据库dbName,所以可以根据dbName创建不同的数据源”。

1、    首先在配置文件中配置一个默认数据源。
在ApplicationContext.xml中配置数据源。
(1)    配置sqlSessionFactory,指定数据源为dynamicDataSource:

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

(2)    配置dynamicDataSource,为自定义的DynamicDataSource类,负责切换数据源和动态增加数据源,指定了默认数据源为jdbcDataSource_nbr_bx:

  1. <bean id="dynamicDataSource" class="com.bx.erp.action.interceptor.DynamicDataSource" primary="true">
  2. <property name="targetDataSources">
  3. <map key-type="java.lang.String">
  4. <entry value-ref="jdbcDataSource_nbr_bx" key="jdbcDataSource_nbr_bx" />
  5. map>
  6. property>
  7. <property name="defaultTargetDataSource" ref="jdbcDataSource_nbr_bx" />
  8. bean>

(3)    配置默认数据源jdbcDataSource_nbr_bx:

  1. <bean id="jdbcDataSource_nbr_bx" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  2. <property name="driverClassName">
  3. <value>${driverClassName}value>
  4. property>
  5. <property name="url">
  6. <value>jdbc:mysql://localhost:3306/nbr_bx?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULLvalue>
  7. property>
  8. <property name="username">
  9. <value>${db.nbx.mysql.username}value>
  10. property>
  11. <property name="password">
  12. <value>${db.nbx.mysql.password}value>
  13. property>
  14. bean>

2、    新建DynamicDataSource类来实现切换不同的数据源。
Spring框架有一个AbstractRoutingDataSource类,它可以在程序运行的时候,把某个数据源动态织入到程序中,以便访问不同的数据库。它有一个determineCurrentLookupKey方法,该方法返回一个键key,通过这个key,在数据源集合中获取其中一个数据源。最后根据获取到的数据源去访问数据库。所以DynamicDataSource类可以继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法,通过返回不同的key实现切换不同的数据源,默认使用配置文件中的数据源。

  1. public class DynamicDataSource extends AbstractRoutingDataSource {
  2. @Override
  3. protected Object determineCurrentLookupKey() {
  4. String dbName = DataSourceContextHolder.getDbName();
  5. if (dbName == null) {
  6. dbName = DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx; // 默认使用公共DB:nbr_bx
  7. } else {
  8. this.selectDataSource(dbName);
  9. if (dbName.equals(DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx)) {
  10. dbName = DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx;
  11. }
  12. }
  13. logger.debug("--------> using datasource " + dbName);
  14. return dbName;
  15. }
  16. ……

3、    DynamicDataSource实现动态增加数据源。
我们在配置文件中只配置了一个默认数据源,那么需要增加数据源,才能够切换不同的数据源访问不同的数据库。
增加一个数据源的步骤如下:
例如我们要访问一个名称为nbr_test的数据库,这个访问操作对应着某一个线程,线程要访问哪一个数据库,我们可以使用ThreadLocal类来设置和获取。ThreadLocal类可以防止一个线程的变量被其它线程修改。
我们用DataSourceContextHolder封装ThreadLocal:

  1. public class DataSourceContextHolder{
  2. public static final String DATASOURCE_jdbcDataSource_nbr_bx = "jdbcDataSource_nbr_bx";
  3. private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
  4. public static void setDbName(String dbName) {
  5. contextHolder.set(dbName);
  6. }
  7. public static String getDbName() {
  8. return ((String) contextHolder.get());
  9. }
  10. public static void clearDbName() {
  11. contextHolder.remove();
  12. }
  13. }

在访问数据库前,我们设置访问nbr_test这个数据库:

DataSourceContextHolder.setDbName(dbName);

DynamicDataSource的determineCurrentLookupKey方法获取到当前线程的数据库名称dbName:

String dbName = DataSourceContextHolder.getDbName();

DynamicDataSource的selectDataSource方法从 targetDataSources这个hashmap中查找是否存在这个数据源,因为现在还没有这个数据源,所以需要去创建,后面创建好了之后,也会把创建好的数据源存到这个targetDataSources中:

  1. /** @describe 数据源存在时不做处理,不存在时创建新的数据源链接,并将新数据链接添加至缓存 */
  2. public void selectDataSource(String dbName) {
  3. String sid = DataSourceContextHolder.getDbName();
  4. if (DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx.equals(dbName)) {
  5. DataSourceContextHolder.setDbName(DataSourceContextHolder.DATASOURCE_jdbcDataSource_nbr_bx);
  6. return;
  7. }
  8. Object obj = this._targetDataSources.get(dbName);
  9. if (obj != null && sid.equals(dbName)) {
  10. return;
  11. } else {
  12. BasicDataSource dataSource = this.getDataSource(dbName);
  13. if (null != dataSource)
  14. this.setDataSource(dbName, dataSource);
  15. }
  16. }

     创建一个数据源:

  1. public BasicDataSource getDataSource(String serverId) {
  2. return (BasicDataSource) createDataSource(serverId);
  3. }
  4. /** 该方法为同步方法,防止并发创建两个相同的数据库 使用双检锁的方式,防止并发 */
  5. private synchronized DataSource createDataSource(String dbName) {
  6. String url = String.format(DB_MYSQL_URL, dbName);
  7. //
  8. BasicDataSource dataSource = new BasicDataSource();
  9. dataSource.setDriverClassName(DRIVER_CLASS_NAME);
  10. dataSource.setUrl(url);
  11. dataSource.setUsername(DB_MYSQL_USERNAME);
  12. dataSource.setPassword(DB_MYSQL_PASSWORD);
  13. dataSource.setTestWhileIdle(true);
  14. dataSource.setValidationQuery("select 1");
  15. dataSource.setTestOnBorrow(true);
  16. return dataSource;
  17. }

       数据库连接信息放在了一个配置文件中,通过注解@Value来获取,访问不同的的数据库只是dbName这个字段不同,所以可以使用String.format(DB_MYSQL_URL, dbName)来修改连接url,访问不同的数据库。

       注解:

  1. @Value("${driverClassName}")
  2. private String DRIVER_CLASS_NAME;
  3. @Value("${db.mysql.url}")
  4. private String DB_MYSQL_URL;
  5. @Value("${db.mysql.username}")
  6. private String DB_MYSQL_USERNAME;
  7. @Value("${db.mysql.password}")
  8. private String DB_MYSQL_PASSWORD;

配置文件:

  1. # 数据库驱动
  2. driverClassName=com.mysql.cj.jdbc.Driver
  3. # 数据库URL
  4. db.mysql.url=jdbc:mysql://localhost:3306/%s?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL
  5. # 数据库用户名
  6. db.mysql.username.encryption=81A3FD464E18C4497A79CE7CC9D5B660
  7. db.nbx.mysql.username.encryption=81A3FD464E18C4497A79CE7CC9D5B660
  8. # 密码
  9. db.mysql.password.encryption=16CCEF25E22FA42E89821B7B27858DE26DD8BFF1139FF2C70BECCA88E373F809
  10. db.nbx.mysql.password.encryption=16CCEF25E22FA42E89821B7B27858DE26DD8BFF1139FF2C70BECCA88E373F809

     创建好一个数据源后,把它放到集合缓存中,下次就不用重新创建了,同时告诉父类AbstractRoutingDataSource,要使用当前这个数据源访问数据库:

  1. BasicDataSource dataSource = this.getDataSource(dbName);
  2. if (null != dataSource)
  3. this.setDataSource(dbName, dataSource);
  4. public void setDataSource(String serverId, BasicDataSource dataSource) {
  5. this.addTargetDataSource(serverId, dataSource);
  6. DataSourceContextHolder.setDbName(serverId);
  7. }
  8. public void addTargetDataSource(String key, BasicDataSource dataSource) {
  9. this._targetDataSources.put(key, dataSource);
  10. this.setTargetDataSources(this._targetDataSources);
  11. }
  12. public void setTargetDataSources(Map<Object, Object> targetDataSources) {
  13. this._targetDataSources = targetDataSources;
  14. super.setTargetDataSources(this._targetDataSources);
  15. afterPropertiesSet();
  16. }

4、    使用Spring的AOP,将线程ThreadLocal的dbName变量清空,防止影响下一次访问数据库。

  1. @Aspect // for aop
  2. @Component // for auto scan
  3. @Order(0) // 在事务前执行
  4. public class DataSourceInterceptor {
  5. @Pointcut("execution(public * com.bx.erp.action.bo.*.*(..))")
  6. public void dataSourceSlave() {
  7. };
  8. @Before("dataSourceSlave()")
  9. public void before(JoinPoint jp) {
  10. // System.out.println("进入切面");
  11. }
  12. @After("dataSourceSlave()")
  13. public void removeDataSoruce(JoinPoint joinPoint) throws Throwable {
  14. DataSourceContextHolder.clearDbName();
  15. }

5、    在我们的产品中,不同的公司对应着不同的数据库dbName,所以可以根据dbName创建不同的数据源。

  1. Company company = getCompanyFromSession(session);
  2. String dbName = company.getDbName();
  3. DataSourceContextHolder.setDbName(dbName);

原文:https://blog.csdn.net/weixin_44071138/article/details/122184340

联系站长

QQ:769220720