1. 概述
仓库地址:https://gitee.com/aopmin/multi-datasource-demo
随着项目规模的扩大和业务需求的复杂化,单一数据源已经不能满足实际开发中的需求。在许多情况下,我们需要同时操作多个数据库,或者需要将不同类型的数据存储在不同的数据库中。这时,多数据源场景成为必不可少的解决方案。
市面上常见的多数据源实现方案如下:
-
方案1:基于Spring框架提供的AbstractRoutingDataSource。
- 优点: 简单易用,支持动态切换数据源;适用于少量数据源情况。
- 场景:适用于需要动态切换数据源,且数据库较少的情况。
- 文档地址:
-
方案2:使用MP提供的Dynamic-datasource多数据源框架。
- 文档地址:https://baomidou.com/guides/dynamic-datasource/#dynamic-datasource
-
方案3:通过自定义注解在方法或类上指定数据源,实现根据注解切换数据源的功能。
- 优点: 灵活性高,能够精确地控制数据源切换;在代码中直观明了。
- 场景: 适用于需要在代码层面进行数据源切换,并对数据源切换有精细要求的情况。
-
方案4:使用动态代理技术,在运行时动态切换数据源,实现多数据源的切换。
- 优点: 灵活性高,支持在运行时动态切换数据源;适合对数据源切换的逻辑有特殊需求的情况。
- 场景: 适用于需要在运行时动态决定数据源切换策略的情况。
-
…
2. 基于SpringBoot的多数据源实现方案
1、执行sql脚本:(分别创建两个数据库,里面都提供一张user表)
-- 创建数据库ds1
CREATE DATABASE `ds1`;
-- 使用ds1数据库
USE ds1;
-- 创建user表
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键id',
`username` VARCHAR(50) COMMENT '用户名',
`gender` TINYINT(1) COMMENT '性别:0男,1女'
);
-- 向user表插入数据
INSERT INTO user (username, gender) VALUES
('张三', 1),
('李四', 0),
('王五', 1);
-- 创建数据库ds2
CREATE DATABASE `ds2`;
-- 使用ds2数据库
USE ds2;
-- 创建user表
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键id',
`username` VARCHAR(50) COMMENT '用户名',
`gender` TINYINT(1) COMMENT '性别:0男,1女'
);
-- 向user表插入数据
INSERT INTO user (username, gender) VALUES
('赵六', 1),
('陈七', 0),
('宝国', 1);
2、创建一个maven工程,向pom.xml中添加依赖:
<!--锁定SpringBoot版本-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!--jdbc起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--test起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--jdbc起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
3、编写实体类:
package cn.aopmin.entity;
import lombok.*;
/**
* 实体类
*
* @author 白豆五
* @since 2024/7/4
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private Integer id;
private String username;
private Integer gender;
}
4、创建application.yml文件,配置数据源:
spring:
#动态数据源配置
datasource:
ds1:
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/ds1?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
ds2:
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/ds2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
logging:
level:
cn.aopmin: debug
5、编写数据源配置类:
package cn.aopmin.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源配置类
* 配置多数据源和动态数据源
*
* @author 白豆五
* @since 2024/7/4
*/
@Configuration
public class DataSourceConfig {
//定义数据源1
@Bean("ds1")
@ConfigurationProperties(prefix = "spring.datasource.ds1")
public DataSource ds1() {
return DataSourceBuilder.create().build();
}
//定义数据源2
@Bean("ds2")
@ConfigurationProperties(prefix = "spring.datasource.ds2")
public DataSource ds2() {
return DataSourceBuilder.create().build();
}
//定义动态数据源
@Bean(name = "dataSource")
public DataSource dynamicDataSource(@Qualifier("ds1") DataSource ds1,
@Qualifier("ds2") DataSource ds2) {
//1.定义数据源map
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("ds1", ds1);
targetDataSources.put("ds2", ds2);
//2.实例化自定义的DynamicDataSource对象, 并设置数据源map
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
//3.设置默认数据源,未匹配上则使用默认数据源
dynamicDataSource.setDefaultTargetDataSource(ds1);
return dynamicDataSource;
}
// 通过JdbcTemplate
@Bean
public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource ds) {
return new JdbcTemplate(ds);
}
}
6、创建DynamicDataSource动态数据类:
package cn.aopmin.config;
import cn.aopmin.common.DataSourceContextHolder;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* AbstractRoutingDataSource(抽象的数据源路由器) 的基本原理是, 它维护了一个数据源的集合,每个数据源都有唯一的一个标识符
* 当应用程序需要访问数据库的时候,AbstractRoutingDataSource会根据某种匹配规则(例如请求参数、用户身份等)来选择一个合适的数据源,
* 并将请求转发给这个数据源。
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 获取数据源名称
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
7、定义一个ThreadLocal工具类:
package cn.aopmin.common;
/**
* 使用ThreadLocal保存数据源名称
*
* @author 白豆五
* @since 2024/7/4
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 将数据源名称绑定到当前线程上
public static void setDataSource(String dataSourceName) {
contextHolder.set(dataSourceName);
}
// 获取当前线程上的数据源名称
public static String getDataSource() {
return contextHolder.get();
}
// 清除数据源名称
public static void clearDataSource() {
contextHolder.remove();
}
}
8、创建启动类
package cn.aopmin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 启动类
*
* @author 白豆五
* @since 2024/7/3
*/
@SpringBootApplication
public class Demo01Application {
public static void main(String[] args) {
SpringApplication.run(Demo01Application.class, args);
}
}
9、创建UserService:
package cn.aopmin.service;
import cn.aopmin.common.DataSourceContextHolder;
import cn.aopmin.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
/**
* @author 白豆五
* @since 2024/7/4
*/
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void insertDs1(User user) {
try {
// todo:自定义注解+SpringAop实现数据源的切换
DataSourceContextHolder.setDataSource("ds1");
String sql = "insert into user(username,gender) values(?,?)";
jdbcTemplate.update(sql,user.getUsername(), user.getGender());
} finally {
DataSourceContextHolder.clearDataSource();
}
}
public void insertDs2(User user) {
try {
DataSourceContextHolder.setDataSource("ds2");
String sql = "insert into user(username,gender) values(?,?)";
jdbcTemplate.update(sql,user.getUsername(), user.getGender());
} finally {
DataSourceContextHolder.clearDataSource();
}
}
}
10、编写测试:
package cn.aopmin.service;
import cn.aopmin.Demo01Application;
import cn.aopmin.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest(classes = {Demo01Application.class})
public class UserServiceTest {
@Resource
private UserService userService;
@Test
public void testInsertDs1() {
User user = new User();
user.setUsername("jack");
user.setGender(0);
user.setGender(1);
userService.insertDs1(user);
}
@Test
public void testInsertDs2() {
User user = new User();
user.setUsername("rose");
user.setGender(1);
userService.insertDs2(user);
}
}
最终效果:
3. 基于Dynamic-datasource实现方案
mp文档:https://baomidou.com/guides/dynamic-datasource/#_top
1、创建SpringBoot工程,引入Dynamic-datasource依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
2、配置数据源:
spring:
#多数据源配置
datasource:
dynamic:
primary: master #设置默认数据源
strict: false #是否严格检查动态数据源提供的数据库名
datasource:
#数据源1
master:
url: jdbc:mysql:///ds1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
#数据源2
slave1:
url: jdbc:mysql:///ds2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
3、实体类:
package cn.aopmin.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 实体类
*
* @author 白豆五
* @since 2024/7/4
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String username;
private Integer gender;
}
4、业务类:
package cn.aopmin.service;
import cn.aopmin.entity.User;
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
/**
* 通过@DS注解切换数据源
* @author 白豆五
* @since 2024/7/4
*/
@Service
// @DS("master") //不加@DS注解,会使用默认数据源
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void insertDs1(User user) {
String sql = "insert into user(username,gender) values(?,?)";
jdbcTemplate.update(sql,user.getUsername(), user.getGender());
}
@DS("slave1")
public void insertDs2(User user) {
String sql = "insert into user(username,gender) values(?,?)";
jdbcTemplate.update(sql,user.getUsername(), user.getGender());
}
}
4、测试类:
package cn.aopmin.service;
import cn.aopmin.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
public class UserServiceTest2 {
@Resource
private UserService userService;
@Test
public void testInsertDs1() {
User user = new User();
user.setUsername("jack");
user.setGender(0);
user.setGender(1);
userService.insertDs1(user);
}
@Test
public void testInsertDs2() {
User user = new User();
user.setUsername("rose");
user.setGender(1);
userService.insertDs2(user);
}
}