利用AOP编程实现springboot多数据源的切换

为什么需要多数据源切换

在正式的、较大的项目中,很少会只有一个数据库的,至少都会有一个主数据库和一个从数据库,从数据库作为数据备份以及数据读取,而主库作为数据写入;因此就产生了一个问题——如何在项目中做到数据源的切换,如何根据操作进行数据库主从的切换。

解决

在利用springboot集成mybatis的时候,mybatis的sessionFactory由springboot进行创建,而sessionFactory由依赖于数据库连接,因此,首先就是需要解决数据源的问题,这里,利用springboot的注入功能,去注入三个DataSource的bean——masterDataSourceslaveDataSource以及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多数据源和动态数据源配置