Spring Cloud + MyBatis + Druid 动态数据源实现

最近公司某项目计划对数据库进行水平分库处理,因此需要实现程序根据参数自动切换数据库的功能。

本文讲述如何在 Spring Cloud + MyBatis + Druid + Oracle 环境下实现动态数据源切换的功能。

配置数据源

在两个 Oracle 数据库创建 Person 表,并插入几行数据:

1
2
3
4
5
6
7
8
9
create table Person
(
id NUMBER(8) not null,
name VARCHAR2(64) not null
)

INSERT INTO Person (id, name) VALUES (1, 'Leo');
INSERT INTO Person (id, name) VALUES (2, 'Lee');
INSERT INTO Person (id, name) VALUES (3, 'Lih');

为便于标识不同数据库的数据,可以将第三行 insert 语句改为:

1
INSERT INTO Person (id, name) VALUES (3, 'LeoLee');

创建一个 Spring Boot 项目,其 application.yml 文件添加两个 Oracle 库的连接信息:

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
spring:
application:
name: multisource-service

datasource:
druid:
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,"

stat-view-servlet:
enabled: true
login-username: admin
login-password: admin
reset-enable: false
url-pattern: /druid/*

total:
url: jdbc:oracle:thin:@(DESCRIPTION = (ADDRESS_LIST =(ADDRESS = (PROTOCOL = TCP) (HOST = 10.88.115.103 )(PORT = 1521))) (CONNECT_DATA = (SERVICE_NAME = trade) ) )
username: user
password: passwd
driver-class-name: oracle.jdbc.OracleDriver
initial-size: 32
max-active: 32
min-idle: 5
max-wait: 60000

part1:
url: jdbc:oracle:thin:@(DESCRIPTION = (ADDRESS_LIST =(ADDRESS = (PROTOCOL = TCP) (HOST = 10.88.115.94 )(PORT = 1521))) (CONNECT_DATA = (SERVICE_NAME = trade) ) )
username: user
password: passwd
driver-class-name: oracle.jdbc.OracleDriver
initial-size: 32
max-active: 32
min-idle: 5
max-wait: 60000

mybatis:
mapperLocations: classpath:mapper/*.xml

由于使用 Druid 作为数据库连接池,故添加了 Druid 连接池的配置,并添加了 Druid 监控页面的配置。有关 Druid 的使用,可以参考 《Spring Boot Druid 使用教程》

pom.xml 依赖如下:

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc8</artifactId>
<version>12.2.0.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

由于需要连接 oracle 数据库,故引入了 ojdbc8 依赖,ojdb8 下载链接:https://www.oracle.com/database/technologies/jdbc-ucp-122-downloads.html。下载后,可以安装到本地然后引入 pom.xml 使用,如何安装本地 Maven 依赖,请自行百度或 Google。

开发动态数据源

1、定义数据源枚举类型

DatabaseType 定义如下,这里定义了两个数据源,读者可以根据实际情况,自行添加调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum DatabaseType {
TOTAL("total", "1"),
PART1("part1", "2");

private final String name;
private final String value;

DatabaseType(String name, String value) {
this.name = name;
this.value = value;
}

public String getName() {
return name;
}

public String getValue() {
return value;
}
}

2、定义线程安全的数据源容器

定义 DatabaseContextHolder

1
2
3
4
5
6
7
8
9
10
11
public class DatabaseContextHolder {
private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();

public static void setDatabaseType(DatabaseType type){
contextHolder.set(type);
}

public static DatabaseType getDatabaseType(){
return contextHolder.get();
}
}

需要定义一个线程安全的容器,用来存储当前使用的数据源,这是由于在客户端并发请求环境下,避免不同线程之间数据相互影响。

3、定义数据源获取方法

定义 determineCurrentLookupKey 方法,这个方法会在框架获取数据源时会被自动调用(读者可以自行设置断点测试一下)。这样一来,通过提前设置好数据源,再调用 determineCurrentLookupKey 方法,即可实现动态数据源的目的。

1
2
3
4
5
public class DynamicDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDatabaseType();
}
}

4、配置 MyBatis

MybatisConfig 中, 创建两个 数据源 DataSource ,分别为 dataSourceTotaldataSourcePart1,并创建动态数据源 dynamicDataSource,最后根据动态数据源创建 SqlSessionFactory ,即 sessionFactory

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
@Configuration
@MapperScan(basePackages="me.leehao.multidbsource.mapper", sqlSessionFactoryRef="sessionFactory")
public class MybatisConfig {
@Autowired
Environment environment;

@Bean(name = "dataSourceTotal")
@ConfigurationProperties(prefix = "spring.datasource.druid.total")
public DataSource getDateSourceTotal()
{
return DruidDataSourceBuilder.create().build();
}

@Bean(name = "dataSourcePart1")
@ConfigurationProperties(prefix = "spring.datasource.druid.part1")
public DataSource getDateSourcePart1()
{
return DruidDataSourceBuilder.create().build();
}


@Bean(name="dynamicDataSource")
public DynamicDataSource DataSource(@Qualifier("dataSourceTotal") DataSource dataSourceTotal,
@Qualifier("dataSourcePart1") DataSource dataSourcePart1){
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DatabaseType.TOTAL, dataSourceTotal);
targetDataSource.put(DatabaseType.PART1, dataSourcePart1);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
dataSource.setDefaultTargetDataSource(dataSourceTotal);

return dataSource;
}

@Bean(name="sessionFactory")
public SqlSessionFactory sessionFactory(@Qualifier("dynamicDataSource")DynamicDataSource dataSource)
throws Exception{
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactoryBean.setMapperLocations(
resolver.getResources(Objects.requireNonNull(environment.getProperty("mybatis.mapperLocations"))));
return sessionFactoryBean.getObject();
}
}

为指定 Mapper 类的扫描路径,使用了注解 @MapperScan(basePackages="me.leehao.multidbsource.mapper", 。有关如何使用 MyBatis ,可以参考文章 《Spring Boot MyBatis 学习教程》

5、开发 REST 测试接口

为测试根据请求参数动态选择数据源的功能,定义接口 /person

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class PersonController {
@Resource
private PersonService personService;

@RequestMapping(value = "/person", method = RequestMethod.GET)
public String errMsg(@RequestParam(value="db", required=true)String db,
@RequestParam(value="id", required=true)Integer id) {
return personService.getPerson(db, id);
}
}

接口 /person 根据 db 参数来确定使用哪个数据库。

运行

调用 http://127.0.0.1:8080/person?db=1&id=3,返回:

LeoLee

调用 http://127.0.0.1:8080/person?db=2&id=3,返回

Lih

说明实现了根据不同请求参数选择不同数据源的功能。

Druid 监控

打开 Druid 监控页面,可以看到两个数据源的监控数据:

附:完整源代码

https://github.com/haozlee/springcloudstudy/tree/main/multidbsource

参考资料