Springboot--之--动态数据源切换 | Eloise's Paradise
0%

Springboot--之--动态数据源切换

本文来介绍工作中很常见的一个需求: 动态切换数据源 的理论原理及其实现.

动态数据源切换

实现

思路:

首先 默认读者知道实现动态数据源切换是使用的SpringBoot中 AbstractRoutingDataSource类结合注解和AOP来实现的.
有了这个理论前提, 那么再来构建思路就会很方便.

  1. 自定义一个注解@TargetDataSource, 将来可以将该注解加在某个service类或者其中的方法上, 通过value属性来指定被修饰的类或者方法最终应该使用哪一个数据源.
  2. 针对第一步, 对于被修饰的类或者方法, 其要使用的数据源名称需要在使用时方便取出, 所以很自然想到存到ThreadLocal.
  3. 定义切面, 在其中定义好切入点和环绕通知.(之所以是环绕通知是因为要在业务方法执行前确定使用过的数据源是哪一个并放入ThreadLocal, 并且在使用后从ThreadLocal中删除, 避免内存泄漏).
  4. 当Mapper执行的时候, 需要DataSource时, 会自动通过AbstractRoutingDataSource中去查找需要的数据源, 只需要返回ThreadLocal中保存的值即可.

代码实现

定义注解

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
package com.boy.springbootallroutingdatasource.annotation;

import com.boy.springbootallroutingdatasource.datasource.DataSourceType;

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

/**
* @author Joshua.H.Brooks
* @description 这个注解将来可以加载某个service或者方法上, 通过value属性来指定类或者方法最终应该使用哪一个数据源
* @date 2022-09-07 20:42
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface TargetDataSource {
/**
* 如果一个方法上使用了该注解@TargetDataSource, 但是没有指定数据源的名称,即属性value没有显式赋值
* 那么默认使用db01
*
* @return
*/
String value() default DataSourceType.DEFAULT_DATASOURCE_NAME;
}

定义ContextHolder

也就是用来存取数据源名称的ThreadLocal变量 和 存取数据源名称的方法.

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
package com.boy.springbootallroutingdatasource.context;

/**
* @author Joshua.H.Brooks
* @description 这个类用来存储当前线程所使用的数据源的名称。
* service层拦截, 指定为对应的数据源后再给mapper使用, 这一切都发生在同一个线程内,所以很容易想到使用ThreadLocal来存储。
* @date 2022-09-07 20:51
*/
public class DynamicDatasourceContextHolder {
private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

/**
* 存入
*
* @param dbType
*/
public static void setDataSourceType(String dbType) {
CONTEXT_HOLDER.set(dbType);
}

/**
* 读取
*/
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}

/**
* 用完需要清空,否则容易内存泄漏 "memory leakage"
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}

定义切面

定义了切入点表达式和环绕通知, 其中在环绕通知中对逻辑: “在业务方法执行前确定使用过的数据源是哪一个并放入ThreadLocal, 并且在使用后从ThreadLocal中删除, 避免内存泄漏.” 进行了实现.

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.boy.springbootallroutingdatasource.aspect;

import com.boy.springbootallroutingdatasource.annotation.TargetDataSource;
import com.boy.springbootallroutingdatasource.context.DynamicDatasourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
* @author Joshua.H.Brooks
* @description
* @date 2022-09-07 21:01
*/
@Aspect
@Order(1)
@Component
public class DatasourceAspect {
/**
* 定义切入点表达式
*
* @annotation(com.boy.springbootallroutingdatasource.annotation.TargetDataSource) 表示如果该注解修饰某个方法, 则拦截该方法
* @within(com.boy.springbootallroutingdatasource.annotation.TargetDataSource) 表示如果该注解修饰某个类, 则拦截该类中所有方法
*/
@Pointcut("@annotation(com.boy.springbootallroutingdatasource.annotation.TargetDataSource) ||" +
"@within(com.boy.springbootallroutingdatasource.annotation.TargetDataSource)")
public void datasourcePointCut() {
}


/**
* 定义环绕通知
*
* @param pjp
* @return
*/
@Around("datasourcePointCut()")
public Object around(ProceedingJoinPoint pjp) {
//1. 获取方法上的有效注解。(方法优先级大于类上)
TargetDataSource targetDataSource = getDataSource(pjp);
if (targetDataSource != null) {
String datasource = targetDataSource.value();
//将获取到的数据源名称存入threadlocal
DynamicDatasourceContextHolder.setDataSourceType(datasource);
}
try {
return pjp.proceed(); //继续下一个通知(如有),或者 触发最终业务方法执行。
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
DynamicDatasourceContextHolder.clearDataSourceType(); // 用完后删除
}
return null;
}

/**
* @param pjp
* @return
*/
private TargetDataSource getDataSource(ProceedingJoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
TargetDataSource methodAnnotation = AnnotationUtils.findAnnotation(methodSignature.getMethod(), TargetDataSource.class);
if (methodAnnotation != null) { // 如果方法上有@TargetDataSource注解, 直接将其返回, 因为方法上的注解优先级比类上的高, 所以不需要再往下了
return methodAnnotation;
}
return AnnotationUtils.findAnnotation(methodSignature.getDeclaringType(), TargetDataSource.class); //否则返回类上的注解
}
}

定义数据源属性类

根据yml文件里设定的数据源信息(或者也是根据DruidDataSource, 因为也没了也是根据其autoconfiguration里的设定来的), 来定义该类的属性.

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.boy.springbootallroutingdatasource.properties;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

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

/**
* @author Joshua.H.Brooks
* @description
* @date 2022-09-07 21:30
*/
@Data
@ToString
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidDataSourceProperties {
String type;
String driverClassName;
Map<String, Map<String, String>> ds;
//初始连接数
Integer initialSize;
//最小连接池数量
Integer minIdle;
//最大连接池数量
Integer maxActive;
//配置获取连接等待超时的时间
Integer maxWait;
//配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
Integer timeBetweenEvictionRunsMillis;
//配置一个连接在池中最小生存的时间,单位是毫秒
Integer minEvictableIdleTimeMillis;
//配置一个连接在池中最大生存的时间,单位是毫秒
Integer maxEvictableIdleTimeMillis;
//配置检测连接是否有效
String validationQuery;
boolean testWhileIdle;
boolean testOnBorrow;
boolean testOnReturn;
Map<String, String> webStatFilter;
Map<String, String> statViewServlet;
Map<String, Map<String, String>> filter;

/**
* 在外部构造好一个只包含三个核心属性:url,username,password的数据源: druidDataSource.
* 然后调用次方法设置公共属性。
*
* @param druidDataSource
* @return
*/
public DataSource initCommonAttributes(DruidDataSource druidDataSource) {
druidDataSource.setInitialSize(initialSize);
druidDataSource.setMinIdle(minIdle);
druidDataSource.setMaxActive(maxActive);
druidDataSource.setMaxWait(maxWait);
druidDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
druidDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
druidDataSource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
druidDataSource.setValidationQuery(validationQuery);
druidDataSource.setTestWhileIdle(testWhileIdle);
druidDataSource.setTestOnBorrow(testOnBorrow);
return druidDataSource;
}
}

定义数据源加载器类

该类用于读取所有数据源, 这里准备好可以供后面AbstractRoutingDataSource随意使用, 指定使用哪一个.

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
package com.boy.springbootallroutingdatasource.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.boy.springbootallroutingdatasource.properties.DruidDataSourceProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

/**
* @author Joshua.H.Brooks
* @description 该类用于读取所有数据源
* @date 2022-09-07 21:51
*/
@Component
public class DataSourceLoader {
@Autowired
DruidDataSourceProperties druidDataSourceProperties;

public Map<String, DataSource> loadAllDataSources(){
HashMap<String, DataSource> map = new HashMap<>();
//获取核心属性的数据源
Map<String, Map<String, String>> ds = druidDataSourceProperties.getDs();
// 对ds遍历, 然后挨个初始化公共属性
Set<String> keys = ds.keySet();
for (String key : keys) {
Map<String, String> jdbcUrlWithUsernameAndPassword = ds.get(key); //对应yml中JDBC的三个核心元素: url,username,password
try {
//1. DruidDataSourceFactory.createDataSource(jdbcUrlWithUsernameAndPassword)); Druid的数据源工厂类根据属性map就可以创建一个数据源
//2. 对拿到的数据源设置公共参数, 需要强转成DruidDataSource
//3. 最后存入map, key是yml里的db01, db02等, 后面的value就是druiddatasource。
map.put(key, druidDataSourceProperties.initCommonAttributes((DruidDataSource) DruidDataSourceFactory.createDataSource(jdbcUrlWithUsernameAndPassword)));
} catch (Exception e) {
e.printStackTrace();
}
}
return map;
}
}

定义DynamicDataSource

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
package com.boy.springbootallroutingdatasource.datasource;

import com.boy.springbootallroutingdatasource.context.DynamicDatasourceContextHolder;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

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

/**
* @author Joshua.H.Brooks
* @description
* @date 2022-09-07 22:07
*/
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 加载所有数据源 并设置默认数据源
*
* @param dataSourceLoader
*/
public DynamicDataSource(DataSourceLoader dataSourceLoader) {
Map<String, DataSource> allDS = dataSourceLoader.loadAllDataSources();
//1. 设置所有数据源 hashmap包一层是为了类型转换使其匹配。
super.setTargetDataSources(new HashMap<>(allDS));
//2. 设置默认数据源 对于没有@TargetDataSource注解的方法就使用默认的数据源: db01
super.setDefaultTargetDataSource(allDS.get(DataSourceType.DEFAULT_DATASOURCE_NAME));
//3. 调用
super.afterPropertiesSet();
}

/**
* 该方法用来返回数据源名称。 当系统需要数据源时就会自动调用该方法进行获取。
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDatasourceContextHolder.getDataSourceType();
}
}

定义默认数据源类型

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.boy.springbootallroutingdatasource.datasource;

/**
* @author Joshua.H.Brooks
* @description
* @date 2022-09-07 22:22
*/
public interface DataSourceType {
/**
* 默认数据源名称: db01
*/
String DEFAULT_DATASOURCE_NAME = "db01";
}

测试:

DB和层级代码准备:

DB:

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
# 建库
create database db_01;
create database db_02;
# 建表
create table db_01.t_user
(
id varchar(32) not null comment '主键(自动生成)'
primary key,
remarks varchar(255) not null comment '备注',
create_by varchar(255) not null comment '创建者',
create_date timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_by varchar(255) not null comment '修改者',
update_date datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
del_flag tinyint not null comment '删除标记(0:正常;1:删除;2:审核)',
username varchar(45) not null,
password varchar(96) not null,
name varchar(45) not null,
constraint unique_user_username
unique (username)
)
comment '用户信息表' charset = utf8;

create table db_02.t_user
(
id varchar(32) not null comment '主键(自动生成)'
primary key,
remarks varchar(255) not null comment '备注',
create_by varchar(255) not null comment '创建者',
create_date timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_by varchar(255) not null comment '修改者',
update_date datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间',
del_flag tinyint not null comment '删除标记(0:正常;1:删除;2:审核)',
username varchar(45) not null,
password varchar(96) not null,
name varchar(45) not null,
constraint unique_user_username
unique (username)
)
comment '用户信息表' charset = utf8;
# 插数
INSERT INTO db_01.t_user (id, remarks, create_by, create_date, update_by, update_date, del_flag, username, password, name) VALUES ('1', '用户1', 'Josh', '2022-09-02 12:31:33', 'Josh', '2022-09-02 12:31:33', 0, 'Alex', '123456', '阿勒克斯');

INSERT INTO db_02.t_user (id, remarks, create_by, create_date, update_by, update_date, del_flag, username, password, name) VALUES ('1', '用户1', 'Josh', '2022-09-02 12:30:47', 'Josh', '2022-09-02 12:30:47', 0, 'Alex', '123456', '阿勒克斯');
INSERT INTO db_02.t_user (id, remarks, create_by, create_date, update_by, update_date, del_flag, username, password, name) VALUES ('2', '用户2', 'Josh', '2022-09-02 12:30:47', 'Josh', '2022-09-02 12:30:47', 0, 'Brux', '123456', '布鲁克斯');

User实体类

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
package com.boy.springbootallroutingdatasource.service;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Date;

/**
* @author Joshua.H.Brooks
* @description
* @date 2022-09-07 22:30
*/
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User {
String id;
String remarks;
String createBy;
String updateBy;
Date createDate;
Date updateDate;
char delFalg;
String username;
String password;
String name;
}

Mapper接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.boy.springbootallroutingdatasource.service;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.mybatis.spring.annotation.MapperScan;

import java.util.List;

/**
* @author Joshua.H.Brooks
* @description
* @date 2022-09-07 22:31
*/
@Mapper
public interface UserMapper {
@Select("select * from t_user order by id desc limit 1")
User getMax();
@Select("select * from t_user")
List<User> getAll();
}

service业务代码:

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
package com.boy.springbootallroutingdatasource.service;

import com.boy.springbootallroutingdatasource.annotation.TargetDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* @author Joshua.H.Brooks
* @description
* @date 2022-09-07 22:31
*/
@Service
@TargetDataSource(value = "db02")
public class UserService {
@Autowired
UserMapper userMapper;

public List<User> getAll() {
return userMapper.getAll();
}

//@TargetDataSource(value = "db02")
public User getMax() {
return userMapper.getMax();
}
}

执行测试

同样的测试代码:

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
package com.boy.springbootallroutingdatasource;

import com.boy.springbootallroutingdatasource.service.User;
import com.boy.springbootallroutingdatasource.service.UserMapper;
import com.boy.springbootallroutingdatasource.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class SpringbootAllRoutingDatasourceApplicationTests {

@Autowired
UserService userService;
@Test
void test() {
System.out.println("没加注解的 getAll");
List<User> list = userService.getAll();
list.stream().forEach(System.out::println);
System.out.println("\n\n\n\n");
System.out.println("@TargetDataSource的 getMax");
System.out.println("userService.getMax() = " + userService.getMax());
}
}

场景一. 修饰方法 不加@TargetDataSource注解使用默认数据源 和 加@TargetDataSource并设置属性value 为其指定特定数据源名称.
场景一测试结果

场景二. 修饰类
场景二测试结果

库实现 TO-DO

通过后台库里DataSource表的数据实现而非yml里配置的数据源。

原理TO-DO

-------------本文结束感谢您的阅读-------------