为什么需要多数据源切换
在正式的、较大的项目中,很少会只有一个数据库的,至少都会有一个主数据库和一个从数据库,从数据库作为数据备份以及数据读取,而主库作为数据写入;因此就产生了一个问题——如何在项目中做到数据源的切换,如何根据操作进行数据库主从的切换。
解决
在利用springboot集成mybatis的时候,mybatis的sessionFactory由springboot进行创建,而sessionFactory由依赖于数据库连接,因此,首先就是需要解决数据源的问题,这里,利用springboot的注入功能,去注入三个DataSource的bean——masterDataSource
、slaveDataSource
以及dynamicDataSource
,而dynamicDataSource
这个我们需要加上@Primary
注解,表明这个是优先注入的DataSource对象
properties文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| mysql.url.master=127.0.0.1:3306/dubbo_cloud spring.datasource.master.jdbc-url =jdbc:mysql://${mysql.url.master}?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true spring.datasource.master.username=root spring.datasource.master.password=1203 spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver spring.datasource.master.hikari.auto-commit=true spring.datasource.master.hikari.maximumPoolSize=10 spring.datasource.master.hikari.validation-timeout=1000 spring.datasource.master.hikari.connection-timeout=1000 spring.datasource.master.hikari.max-lifetime=30000
mysql.url.slave=127.0.0.1:3307/dubbo_cloud_slave spring.datasource.slave.jdbc-url =jdbc:mysql://${mysql.url.slave}?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true spring.datasource.slave.username=root spring.datasource.slave.password=1203 spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver spring.datasource.slave.hikari.auto-commit=true spring.datasource.slave.hikari.maximumPoolSize=10 spring.datasource.slave.hikari.validation-timeout=1000 spring.datasource.slave.hikari.connection-timeout=1000 spring.datasource.slave.hikari.max-lifetime=30000
|
java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| ## DynamicDataSource.java
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return JdbcContextHolder.get(); } }
## DataSourceConfigure.java
public class DataSourceConfigure {
@Bean(value = "masterDataSource") @ConfigurationProperties(prefix="spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); }
@Bean(value = "slaveDataSource") @ConfigurationProperties(prefix="spring.datasource.slave") public DataSource salveDataSource() { return DataSourceBuilder.create().build(); }
@Primary @Bean(value = "dynamicDataSource") public DataSource dataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); DataSource master = masterDataSource(); DataSource slave = salveDataSource(); dynamicDataSource.setDefaultTargetDataSource(master); Map<Object, Object> datasource = new HashMap<>(); datasource.put(DataSourceType.MASTER_DB.getType(), master); datasource.put(DataSourceType.SALVE_ONE.getType(), slave); dynamicDataSource.setTargetDataSources(datasource); return dynamicDataSource; } }
|
至此,完成了第一步的多个数据源的注入,现在,该考虑如何实现数据源的切换;其实,在上面的代码中,dataSource()
方法已经实现了动态数据源的切换——通过创建一个DynamicDataSource
对象,而这个DynamicDataSource
对象是继承了springboot的org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
,该类实际充当了DataSource的路由中介, 能在运行时, 根据某种key值来动态切换到真正的DataSource上。而这个key的切换,就是DynamicDataSource
方法中的determineCurrentLookupKey()
现在,该考虑一下如何根据操作实现数据源的切换,首先,要考虑一个问题,就是实际中是多线程的,如何假设有两个业务在运行在两个线程上,一个线程需要读从库,一个线程需要写主库,要如何保证这两个业务的数据源切换不影响对方?这里就需要用ThreadLocal来实现,ThreadLocal可以实现统一初始化值,但是在修改值的时候,每个线程互相不影响,在自己的线程空间中修改Threadlocal而不会影响别的线程空间中Threadlocal中的值
java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| ## DataSourceType.java
public enum DataSourceType {
MASTER_DB(1, "master"),
SALVE_ONE(2, "slave-one") ;
private int value; private String type;
DataSourceType(int value, String type) { this.value = value; this.type = type; }
public int getValue() { return value; }
public String getType() { return type; }
@Override public String toString() { return "DataSourceType{" + "value=" + value + ", type='" + type + '\'' + '}'; } }
## JdbcContextHolder.java
public class JdbcContextHolder { private final static ThreadLocal<String> dbChose = new ThreadLocal<>();
public static String get() { return dbChose.get(); }
public static void set(DataSourceType dbType) { dbChose.set(dbType.getType()); } }
|
现在就剩最后一步了,根据操作切换数据源,这里,就需要用到java强大的注解功能以及aop切面编程
首先创建一个注解@Dynamic
,作用于方法上,实现方法级别的数据源切换
1 2 3 4 5 6 7
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Dynamic {
DataSourceType value() default DataSourceType.MASTER_DB;
}
|
创建好注解后,开始实现方法的aop切面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Slf4j @Aspect @Order(1) @Component public class DataSourceAspect {
@Pointcut("execution(* com.tensor.org.dao.api..*.*(..))") public void aspect() {}
@Before("aspect()") private void switchDataSource(JoinPoint joinPoint) { Object target = joinPoint.getTarget(); String method = joinPoint.getSignature().getName(); Class<?> cls = target.getClass(); Class<?>[] paramTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes(); try { Method m = cls.getMethod(method, paramTypes); if (m != null && m.isAnnotationPresent(Dynamic.class)) { Dynamic dynamic = m.getAnnotation(Dynamic.class); JdbcContextHolder.set(dynamic.value()); log.info("切换数据源完成 : {}", dynamic.value()); } } catch (NoSuchMethodException e) { log.error("switchDataSource NoSuchMethodException Err : {}", e.getMessage()); } } }
|
首先创建一个aop切点,也就是这段代码
1 2
| @Pointcut("execution(* com.tensor.org.dao.api..*.*(..))") public void aspect() {}
|
通过注解@Pointcut
创建了一个切点,范围是com.tensor.org.dao.api
包以及子包下的所有方法,然后,在切点执行前,我们需要做一个操作,也就是数据源切换,JoinPoint
作为织入点,包含了目标对象,当前被织入的方法的信息,然后利用java反射获取方法的信息,如果被@Dynamic
所修饰,获取该注解的value信息,进行数据源的切换
学习的博客地址
csdn——Spring Boot + Mybatis多数据源和动态数据源配置