SpringBoot动态切换多数据源

问题引入

在大型分布式项目中,经常会出现多数据源的情况,比如说mysql结合sqlServer、Oracle等进行数据存储,此时就需要我们通过Spring配值多数据源,在项目中根据实际进行动态切换,按相应的数据库进行CRUD操作。

难点所在

由于项目中的Bean基本都为单例模式,此时如果大量用户不断切换数据库,改变dataSource,会造成严重的资源掠夺问题,显然,此时解决方案有两个思路,一是:以空间换取时间,二是:以时间换区空间。
首先第一种解决方法很简单,在修改dataSource上加入Synchronized,但在高并发环境下是完全不合理的,很影响性能;
而第二种解决方法较为灵活,以“空间换时间”,为每一个线程提供一份变量(利用ThreadLocal),因此可以同时访问并互不干扰。
废话不多说,直接看实现方法!

具体方法

创建SpringBoot项目,pom.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.6.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.capol</groupId>
	<artifactId>multidatasource</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>multidatasource</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>com.microsoft.sqlserver</groupId>
			<artifactId>sqljdbc4</artifactId>
			<version>4.0</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.46</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>1.1.16</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.4.1</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.9</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

在application.yml中进行如下配置

spring:
  datasource:
    sqlserver:
      driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
      jdbc-url: 
      username: 
      password: 
    mysql:
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: 
      username: 
      password: 

    druid:
      db-type: com.alibaba.druid.pool.DruidDataSource

首先定义一个数据库的枚举类

package com.capol.multidatasource.config;

/**
 * @author fuzihao
 * @date 2019/7/26 16:08
 */
public enum DataSourceEnum {
    MYSQL_DATASOURCE,
    SQLSERVER_DATASOURCE
}

其次,有如下的类,持有数据库连接对象与线程中,需要使用ThreadLocal

package com.capol.multidatasource.config;

/**
 * @author fuzihao
 * @date 2019/7/26 15:38
 */
public class DataSourceContextHolder {

    /**
     * 通过ThreadLocal保证线程安全
     */
    private static final ThreadLocal<DataSourceEnum> contextHolder = new ThreadLocal<>();

    /**
     * 设置数据源变量
     * @param dataSourceEnum 数据源变量
     */
    public static void setDataBaseType(DataSourceEnum dataSourceEnum) {
        System.out.println("修改数据源为:" + dataSourceEnum);
        contextHolder.set(dataSourceEnum);
    }

    /**
     * 获取数据源变量
     * @return 数据源变量
     */
    public static DataSourceEnum getDataBaseType() {
        DataSourceEnum dataSourceEnum = contextHolder.get() == null ? DataSourceEnum.MYSQL_DATASOURCE : contextHolder.get();
        System.out.println("当前数据源的类型为:" + dataSourceEnum);
        return dataSourceEnum;
    }

    /**
     * 清空数据类型
     */
    public static void clearDataBaseType() {
        contextHolder.remove();
    }

}

同时sqlSessionFactory需要一个DynamicDataSource,其继承至AbstractRoutingDatasource,如下

package com.capol.multidatasource.config;

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

/**
 * @author fuzihao
 * @date 2019/7/26 14:58
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataBaseType();
    }

}

此时就可以注入各Bean了

package com.capol.multidatasource.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author fuzihao
 * @date 2019/7/26 14:43
 */
@Configuration
public class DataSourceConfig {
    @Bean(name = "sqlserverDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.sqlserver")
    public DataSource getDateSource1() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "mysqlDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource getDateSource2() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DataSource(@Qualifier("sqlserverDataSource") DataSource sqlserverDataSource,
                                        @Qualifier("mysqlDataSource") DataSource mysqlDataSource) {
        //配置多数据源
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceEnum.SQLSERVER_DATASOURCE, sqlserverDataSource);
        targetDataSource.put(DataSourceEnum.MYSQL_DATASOURCE, mysqlDataSource);
        DynamicDataSource dataSource = new DynamicDataSource();
        //多数据源
        dataSource.setTargetDataSources(targetDataSource);
        //默认数据源
        dataSource.setDefaultTargetDataSource(mysqlDataSource);
        return dataSource;
    }
    @Bean(name = "SqlSessionFactory")
    public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        return bean.getObject();
    }
}

由于mysql数据库中有一个t_blog表,故我在这里简单写了下mapper和entity:

package com.capol.multidatasource.mapper;

import com.capol.multidatasource.entity.Blog;
import org.apache.ibatis.annotations.Select;

/**
 * @author fuzihao
 * @date 2019/7/26 15:47
 */
public interface UserMapper {
    @Select("select pk_t_blog as id,f_author as author from t_blog where pk_t_blog=#{id}")
    Blog selectBlog(Integer id);
}
package com.capol.multidatasource.entity;

/**
 * @author fuzihao
 * @date 2019/7/26 15:46
 */
public class Blog {
    private Integer id;
    private String author;

	//getter and setter
}

此时就也可以在测试类中进行测试了:

package com.capol.multidatasource;

import com.capol.multidatasource.config.DataSourceContextHolder;
import com.capol.multidatasource.config.DataSourceEnum;
import com.capol.multidatasource.entity.Blog;
import com.capol.multidatasource.mapper.UserMapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


@RunWith(SpringRunner.class)
@SpringBootTest
@MapperScan(basePackages = "com.capol.multidatasource.mapper")
public class MultidatasourceApplicationTests {

	@Autowired
	@Qualifier("SqlSessionFactory")
	private SqlSessionFactory sqlSessionFactory;
	@Autowired
	private UserMapper userMapper;
	@Test
	public void contextLoads() {
		Blog blog = userMapper.selectBlog(1);
		System.out.println(blog);
	}
}

此时输出实体类对象,若在config中设置默认为sqlServer数据库,或者在测试中插入

DataSourceContextHolder.setDataBaseType(DataSourceEnum.SQLSERVER_DATASOURCE);

发现会报错找不到 t_blog 表,配置成功!

知识扩展1

为了方便我们在Mapper中切换各数据库,可以通过注解和AOP的方式来实现数据源的切换。

首先定义一个注解类:

package com.capol.multidatasource.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义数据源注解,默认为mysql数据源
 * @author fuzihao
 * @date 2019/7/26 16:26
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSourceAnno {
    DataSourceEnum value() default DataSourceEnum.MYSQL_DATASOURCE;
}

其次,给注解切面

package com.capol.multidatasource.config;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @author fuzihao
 * @date 2019/7/26 16:27
 */
@Aspect
@Component
public class DynamicDataSourceAspect {
    @Around("@annotation(dataSourceAnno)")
    public Object aroudOperDeal(ProceedingJoinPoint point,DataSourceAnno dataSourceAnno) throws Throwable {
        DataSourceContextHolder.setDataBaseType(dataSourceAnno.value());
        // 让目标方法继续进行
        Object retVal=point.proceed();
        DataSourceContextHolder.clearDataBaseType();
        return retVal;
    }
}

此时,我们就可以在mapper中,在相应数据库的curd操作上加上此注解,修改对应数据源啦!

知识扩展2

为什么最后需要contextHolder.remove();
首先看下Thread、ThreadLocalMap、ThreadLocal之间的关系
在这里插入图片描述Thread类有属性变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap (具体表现即为图中的各个Entry),所以每个线程往这个ThreadLocal中读写隔离的,并且是互相不会影响的。
一个ThreadLocal只能存储一个Object对象,如果需要存储多个Object对象那么就需要多个ThreadLocal!!!
在这里插入图片描述
可以看到Entry的key指向ThreadLocal为虚线,即表示弱引用

  • 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,该对象仅仅被弱引用关联,那么就会被回收。

即如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

  • 如果线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

解决方法

每次使用完ThreadLocal都调用它的remove()方法清除数据;

  • remove作用?
    remove方法会把这个key对应Entry的值设为空

或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
所以,最佳实践做法应该为:

public class XXX{
	private static ThreadLocal<Integer> threadlocal=new ThreadLocal<>();
	try{
		...
	}
	finally{
		threadlocal.remove();
	}
}

这也解释了为什么我们需要在最后进行contextHolder.remove()。

发布了63 篇原创文章 · 获赞 29 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Hpsyche/article/details/97522402
今日推荐