一、SpringBoot实现多数据源切换的多种方法

在生产环境中,我们会遇到需要跨多个数据源进行操作数据库的情况,下面我将分享出两种实现多数据源切换的方法。

项目代码:https://files.javaxing.com/Java%08Demo/springboot_dynamic_datasource.zip

1. AbstractRoutingDataSource

这种方式的核心是使用Spring提供的AbstractRoutingDataSource抽象类,注入多个数据源。

并且通过AOP的方式自己完成了读写分离,当然 如果不需要读写分离的话,也可以通过AOP的形式完成数据源切换。

1)基础工程

引入依赖

引入 springBoot、mybatis-plus、druid、mysql、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
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>com.javaxing</groupId>
<artifactId>springboot_dynamic_datasource</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>dynamic_01</module>
</modules>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- mybatisplus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>

<!--Druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
</dependencies>
</project>

这里我们不仅实现了切换数据源,还实现了 基础版的读写分离,所以需要用到AOP切片。

2)编辑application.yaml

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
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource1:
url: jdbc:mysql://10.211.55.12:3306/dynamic-order?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: 123123
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
datasource2:
url: jdbc:mysql://10.211.55.12:3306/dynamic-order-02?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: 123123
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver


mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml

1、需要手动指定多个数据源信息,并为每个数据源创建一个名称,如:datasource1、datasource2

2、因为使用了mybatis-plus,所以需要指定Mapper.xml 位置

3)controller、service基本代码

entity

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
public class Order {
private Integer id;

private String name;

private BigDecimal price;

private Integer userId;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public BigDecimal getPrice() {
return price;
}

public void setPrice(BigDecimal price) {
this.price = price;
}

public Integer getUserId() {
return userId;
}

public void setUserId(Integer userId) {
this.userId = userId;
}
}

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/test")
public class TestController {

@Autowired
private OrderService orderService;

@GetMapping("/read")
public List<Order> read(){
return orderService.findAll();
}

@GetMapping("/write")
public void write(){
Order order = new Order();
order.setName(System.currentTimeMillis()+"");
order.setPrice(new BigDecimal(100.00));
orderService.insertSelective(order);
}
}

service

1
2
3
4
5
public interface OrderService{
int insertSelective(Order record);

public List<Order> findAll();
}

service.impl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderServiceImpl implements OrderService{

@Resource
private OrderMapper orderMapper;

@Override
@WR("W")
public int insertSelective(Order record) {
return orderMapper.insertSelective(record);
}

@Override
@WR("R")
public List<Order> findAll(){
return orderMapper.findAll();
}
}

mapper

1
2
3
4
5
6
@Mapper
public interface OrderMapper {
int insertSelective(Order record);

List<Order> findAll();
}

4)配置DataSourceConfig数据源

底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource,自动完成数据源封装。

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
package com.javaxing.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}

@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}

@Bean
public DataSourceTransactionManager transactionManager1(DynamicDataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}

@Bean
public DataSourceTransactionManager transactionManager2(DynamicDataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}

5)AbstractRoutingDataSource注入到bean

  1. 继承Spring 提供的** AbstractRoutingDataSource(带路由功能的数据源)**
    • 他会将数据源封装到key-value的实体里面,我们可以通过key完成数据源切换
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
package com.javaxing.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

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

@Component
@Primary // 将该Bean设置为主要注入Bean
public class DynamicDataSource extends AbstractRoutingDataSource {
// 当前使用的数据源标识
public static ThreadLocal<String> name=new ThreadLocal<>();

// 写
@Autowired
DataSource dataSource1;
// 读
@Autowired
DataSource dataSource2;

// 返回当前数据源标识
@Override
protected Object determineCurrentLookupKey() {
return name.get();

}

@Override
public void afterPropertiesSet() {

// 为targetDataSources初始化所有数据源
Map<Object, Object> targetDataSources=new HashMap<>();
targetDataSources.put("W",dataSource1);
targetDataSources.put("R",dataSource2);
super.setTargetDataSources(targetDataSources);

// 为defaultTargetDataSource 设置默认的数据源
super.setDefaultTargetDataSource(dataSource1);

super.afterPropertiesSet();
}
}

6)AOP切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
@Aspect
public class DynamicDataSourceAspect implements Ordered {

// 前置
@Before("within(com.javaxing.service.impl.*) && @annotation(wr)")
public void before(JoinPoint point, WR wr){
String name = wr.value();
DynamicDataSource.name.set(name);

System.out.println(name);
}

@Override
public int getOrder() {
return 0;
}

// 环绕通知
}

AOP接口

1
2
3
4
5
6
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
String value() default "W";
}

7)实验测试

只需要在 service方法上 注解@WR(“W”),就可以实现 读写分离。

image-20240131115107930

核心本质会通过AOP切片 完成数据源切换。

image-20240131115155630

dynamic-order 写入了一条数据,而dynamic-order-02(读库)不会写入数据。

image-20240131141553039image-20240131141604651

上面如果多数据源实现了读写分离,如果不想实现读写分离的话,也可以通过注解来实现多数据源切换。

只需要在注解里面写上你想要切换的数据源名称:@WR(“Name”)。

2. 使用dynamic-datasource框架(推荐)

dynamic-datasource是MyBaits-plus作者设计的一个多数据源开源方案。使用这个框架需要引入对应的pom依赖。

使用该框架可以非常简单、快速的实现多数据源切换。

1)基础工程

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- mybatisplus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>

<!--Druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>

<!--dynamic-datasource数据源框架-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
</dependencies>

2)application.yaml

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
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
dynamic:
#设置默认的数据源或者数据源组,默认值即为master
primary: master
#严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
master:
url: jdbc:mysql://10.211.55.12:3306/dynamic-order?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: 123123
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1:
url: jdbc:mysql://10.211.55.12:3306/dynamic-order-02?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF8&useSSL=false
username: root
password: 123123
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml

使用dynamic-datasource框架爱的话,数据源要严格按照他们的要求去写的,并且需要给每个数据源命名,如 master、slave_1。

3)controlle等基础代码

controlle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/test")
public class TestController {

@Autowired
private OrderService orderService;

@GetMapping("/read")
public List<Order> read(){
return orderService.findAll();
}

@GetMapping("/write")
public void write(){
Order order = new Order();
order.setName(System.currentTimeMillis()+"");
order.setPrice(new BigDecimal(100.00));
orderService.insertSelective(order);
}
}

service

  • 在需要数据源切换的方法上完成 @DS(“slave_1”) 注解,就可以实现数据源切换。
    • @DS后面的value值 是在application.yaml里面配置的数据源地址名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderServiceImpl implements OrderService{

@Resource
private OrderMapper orderMapper;

@Override
@DS("master")
public int insertSelective(Order record) {
return orderMapper.insertSelective(record);
}

@Override
@DS("slave_1")
public List<Order> findAll(){
return orderMapper.findAll();
}
}

4)实验测试

访问写入controller:http://localhost:8080/test/write ,然后再次查看数据库,会发现 master库多出了一条新的记录。

image-20240131150224037

而slave_1库则依然空空如也。

image-20240131150305517image-20240131150321531