Spring配置多数据源并实现动态切换
在业务增长到一定数量后,我们的数据库会产生大量的数据,而随着数据量的增大,单表或单库的性能已经达到瓶颈,我们会对数据库进行读写分离、分库分表等优化,而Spring提供的多数据源及动态切换在这些场景就可以派上用场了。
介绍一下AbstractRoutingDataSource
AbstractRoutingDataSource
是Spring中的一个抽象类,它的类继承关系如下,查看类注释信息:它是DataSource
的抽象实现,根据查找key将getConnection()
方法调用路由到多个目标数据源中的一个,后者通常是通过线程绑定的事务上下文确定的。Abstract DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.
AbstractRoutingDataSource
的关键属性和方法afterPropertiesSet():在Spring容器填充bean属性后调用,解析目标数据源集合和默认目标数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
// 对目标数据源集合和默认目标数据源进行解析,并填充到resolvedDataSources和resolvedDefaultDataSource属性
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}getConnection():获取数据库连接
1
2
3public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}determineTargetDataSource():确定目标数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 获取查找key
Object lookupKey = determineCurrentLookupKey();
// 根据查找key从已解析数据源集合中获取目标数据源
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}determineCurrentLookupKey():确定当前查找key,不限类型,但是需要和
resolvedDataSources
中的key类型一致==抽象方法,需要子类重写==
1
2
3private Map<Object, DataSource> resolvedDataSources;
protected abstract Object determineCurrentLookupKey();
具体步骤
创建一个Spring Boot项目(只要是Spring项目就可以),引入MySQL驱动、MyBatis、druid、aop(使用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<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.28</version>
</dependency>
<!--可以不用druid-spring-boot-starter,因为我们需要自定义数据源,用不上自动配置-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>创建UserController、UserService、UserDao和UserMapper.xml(个人喜欢自己写SQL语句,也可以用MP等ORM框架),代码省略,只需要写一个查询接口就可以了
在两个数据库中添加两张结构一样的表,各添加一条数据,这里用nickname字段值不同代替主库和从库区别
创建application.yml或application.properties配置文件,添加数据库连接信息,这里如果用主从数据库连接更好,为了简化操作,这里我用两个不同的数据库代替主数据库和从数据库
1
2
3
4
5
6
7
8
9spring:
ds_01:
username: root
password: password1
url: jdbc:mysql://host1:3306/practice?useUnicode=true&characterEncoding=utf-8&useSSL=false
ds_02:
username: root
password: password2
url: jdbc:mysql://host2:3306/practice?useUnicode=true&characterEncoding=utf-8&useSSL=false创建
DataSourceHolder
,使用ThreadLocal
管理查找key,和线程绑定1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class DataSourceHolder {
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
public static void set(String name) {
DATASOURCE_HOLDER.set(name);
}
public static String get() {
return DATASOURCE_HOLDER.get();
}
public static void clear() {
DATASOURCE_HOLDER.remove();
}
}创建
DynamicDataSource
,继承AbstractRoutingDataSource
,重写determineCurrentLookupKey()
方法,通过DataSourceHolder
获取查找key1
2
3
4
5
6
7
8public class DynamicDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return DataSourceHolder.get();
}
}创建
DataSourceEnum
保存数据源信息(可以直接用字符串代替)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public enum DataSourceEnum {
/**
* 主数据源
*/
MASTER("master"),
/**
* 从数据源
*/
SLAVE("slave");
private String name;
DataSourceEnum(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}创建
DataSourceConfig
,配置数据源(可配置任意多个),其中dataSource01
为主库数据源,dataSource02
为从库数据源(实际不是),dynamicDataSource
为[步骤6]中自定义的数据源,并且是主数据源(使用@Primary
注解)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
public class DataSourceConfig {
private Environment environment;
public DataSource dataSource01() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(environment.getProperty("spring.ds_01.username"));
dataSource.setPassword(environment.getProperty("spring.ds_01.password"));
dataSource.setUrl(environment.getProperty("spring.ds_01.url"));
return dataSource;
}
public DataSource dataSource02() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(environment.getProperty("spring.ds_02.username"));
dataSource.setPassword(environment.getProperty("spring.ds_02.password"));
dataSource.setUrl(environment.getProperty("spring.ds_02.url"));
return dataSource;
}
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 将主数据源和从数据源添加到目标数据源集合,设置默认目标数据源
Map<Object, Object> targetDataSource = new HashMap<>(2);
targetDataSource.put(DataSourceEnum.MASTER.getName(), dataSource01());
targetDataSource.put(DataSourceEnum.SLAVE.getName(), dataSource02());
dynamicDataSource.setDefaultTargetDataSource(dataSource01());
dynamicDataSource.setTargetDataSources(targetDataSource);
return dynamicDataSource;
}
}创建自定义注解
SwitchDataSource
,控制使用哪个数据源1
2
3
4
5
6
7
public SwitchDataSource {
DataSourceEnum type() default DataSourceEnum.MASTER;
}创建
DynamicDataSourceAspect
切面,拦截带有@SwitchDataSource
注解的方法,获取注解的name属性作为查找key1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DynamicDataSourceAspect {
public Object around(ProceedingJoinPoint joinPoint, SwitchDataSource switchDataSource) {
DataSourceEnum type = switchDataSource.type();
DataSourceHolder.set(type.getName());
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
} finally {
DataSourceHolder.clear();
}
}
}在UserService的查找方法上添加自定义注解
1
2
3
4
5
6
7
8
9
10
// @SwitchDataSource
public UserDO getById(Long id) {
UserDO user = userMapper.getById(id);
ResponseEnum.USER_NOT_EXIST.assertNotNull(user);
return user;
}对不使用注解、使用注解(不设置type)、使用注解(设置type为slave)三种情况进行测试,查看测试结果
情况1、情况2
情况3
至此,我们就完成了Spring多数据源配置和动态切换了,关于多数据源的事务问题会在后面的篇章中探究。