1、导读
在日常开发中我们都是以单个数据库进行开发,在小型项目中是完全能够满足需求的。
但是,当我们牵扯到像淘宝、京东这样的大型项目的时候,单个数据库就难以承受用户的CRUD操作。
那么此时,我们就需要使用多个数据源进行读写分离的操作,这种方式也是目前一种流行的数据管理方式。
2、所需的资源
- Spring boot (pom不再贴出)
- Mybatis-plus
- Alibab Druid数据库连接池
- MySql 数据库
-
<!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1</version> </dependency> <!-- mybatis-plus代码生成器 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.1.tmp</version> </dependency> <!-- mysql连接 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--mysql druid--> <!--添加数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.16</version> </dependency>
3、Spring Boot配置多数据源
在YAML文件中定义数据源所需的数据
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource ## 声明数据源的类型
mysql-datasource1: ## 声明第一个数据源所需的数据
url: jdbc:mysql://localhost:3306/mybatis?useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mysql-datasource2: ## 声明第二个数据源所需的数据
url: jdbc:mysql://localhost:3306/bookstore?useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid: ## druid数据库连接池的基本初始化属性
initial-size: 5 ## 连接池初始化的大小
min-idle: 1 ## 最小空闲的线程数
max-active: 20 ## 最大活动的线程数
mybatis-plus:
# xml文件路径
mapper-locations: classpath:mapper/*.xml
# 实体类路径
type-aliases-package: com.service.servicea.entity
configuration:
# 驼峰转换
map-underscore-to-camel-case: true
# 是否开启缓存
cache-enabled: false
# 打印sql
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 全局配置
global-config:
# 数据库字段驼峰下划线转换
db-column-underline: true
# id自增类型(数据库id自增)
id-type: 0
mysql-datasource1、mysql-datasource2是自定义的数据
定义多个数据源
import com.alibaba.druid.pool.DruidDataSource;
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 javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean(name = "mysqlDataSource1")
@ConfigurationProperties(prefix = "spring.datasource.mysql-datasource1")
public DataSource dataSource1(){
DruidDataSource build = DruidDataSourceBuilder.create().build();
return build;
}
@Bean(name = "mysqlDataSource2")
@ConfigurationProperties(prefix = "spring.datasource.mysql-datasource2")
public DataSource dataSource2(){
DruidDataSource build = DruidDataSourceBuilder.create().build();
return build;
}
}
@ConfigurationProperties注解用于将YAML中指定的数据创建成指定的对象,但是,YAML中的数据必须要与对象对象中的属性同名,不然无法由Spring Boot完成赋值。
由于我们要定义多个数据源,所以在Spring Boot数据源自动配置类中就无法确定导入哪个数据源来完成初始化,所以我们就需要禁用掉Spring Boot的数据源自动配置类,然后使用我们自定义的数据源配置类来完成数据源的初始化与管理。
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DatasourceDomeApplication {
public static void main(String[] args) {
SpringApplication.run(DatasourceDomeApplication.class, args);
}
}
在启动类上声明需要禁用的自动配置类:exclude = {DataSourceAutoConfiguration.class}
3.1、实现DataSource接口
缺点:产生大量的代码冗余,在代码中存在硬编码。
3.1.1、代码
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
*https://blog.csdn.net/qq_45515182/article/details/126330084
*/
@Component
@Primary
public class DynamicDataSource implements DataSource {
//使用ThreadLocal而不是String,可以在多线程的时候保证数据的可靠性
public static ThreadLocal<String> flag = new ThreadLocal<>();
@Resource
private DataSource mysqlDataSource1; // 注入第一个数据源
@Resource
private DataSource mysqlDataSource2; // 注入第二个数据源
public DynamicDataSource(){ // 使用构造方法初始化ThreadLocal的值
flag.set("read");
}
@Override
public Connection getConnection() throws SQLException {
// 通过修改ThreadLocal来修改数据源,
// 为什么通过修改状态就能改变已经注入的数据源? 这就得看源码了。
if(null == flag.get() || flag.get().equals("read")){
return mysqlDataSource1.getConnection();
}
return mysqlDataSource2.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
}
上面的flag.get()构造中已经赋值了,但在这里还是null,
@Override public Connection getConnection() throws SQLException { // 通过修改ThreadLocal来修改数据源, // 为什么通过修改状态就能改变已经注入的数据源? 这就得看源码了。 if(null == flag.get() || flag.get().equals("read")){ return mysqlDataSource1.getConnection(); } return mysqlDataSource2.getConnection(); }
所以新增 null == flag.get()。欢迎解答!
实现DataSource接口我们本质上只使用了一个方法,就是getConnection()这个无参的方法,但是DataSource接口中所有的方法我们也都需要实现,只是不用写方法体而已,也就是存在了很多的 “废方法” 。
@Primary注解 == @Order(1),用于设置此类的注入顺序。4
3.1.2、使用
@RestController
@RequestMapping("/service-a/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 访问第一个数据库
* @param id
* @return
*/
@GetMapping("/selectUser")
public ResponseEntity<UserEntity> getUser(Long id) {
DynamicDataSource.flag.set("read"); // 修改数据源的状态
UserEntity userEntity = userService.getUserById(id);
return ResponseEntity.ok(userEntity);
}
/**
* 访问第2个数据库
* @param id
* @return
*/
@GetMapping("/selectUser1")
public ResponseEntity<UserEntity> getUser1(Long id) {
DynamicDataSource.flag.set("write"); // 修改数据源的状态
UserEntity userEntity = userService.getUserById(id);
return ResponseEntity.ok(userEntity);
}
}
3.2、继承AbstrictRoutingDataSource类
减少了代码的冗余,但是还是会存在硬编码。
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Primary
@Component
@MapperScan("com.service.servicea.dao")
public class DynamicDataSourceConfigPro extends AbstractRoutingDataSource {
public static ThreadLocal<String> flag = new ThreadLocal<>();
@Resource
private DataSource mysqlDataSource1;
@Resource
private DataSource mysqlDataSource2;
public DynamicDataSourceConfigPro(){
flag.set("read");
}
@Override
protected Object determineCurrentLookupKey() { // 通过Key来得到数据源
return flag.get();
}
@Override
public void afterPropertiesSet() {
Map<Object,Object> targetDataSource = new ConcurrentHashMap<>();
targetDataSource.put("read",mysqlDataSource1);
// 将第一个数据源设置为默认的数据源。
super.setDefaultTargetDataSource(mysqlDataSource1);
targetDataSource.put("write",mysqlDataSource2);
// 将Map对象赋值给AbstrictRoutingDataSource内部的Map对象中。
super.setTargetDataSources(targetDataSource);
super.afterPropertiesSet();
}
}
AbstrictRoutingDataSource的本质就是利用一个Map将数据源存储起来,然后通过Key来得到Value来修改数据源。
3.2.2、使用
@RestController
@RequestMapping("/service-a/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 访问第一个数据库
* @param id
* @return
*/
@GetMapping("/selectUser")
public ResponseEntity<UserEntity> getUser(Long id) {
DynamicDataSourceConfigPro.flag.set("read"); // 修改数据源的状态
UserEntity userEntity = userService.getUserById(id);
return ResponseEntity.ok(userEntity);
}
/**
* 访问第2个数据库
* @param id
* @return
*/
@GetMapping("/selectUser1")
public ResponseEntity<UserEntity> getUser1(Long id) {
DynamicDataSourceConfigPro.flag.set("write"); // 修改数据源的状态
UserEntity userEntity = userService.getUserById(id);
return ResponseEntity.ok(userEntity);
}
}
感谢大佬的文章参考,修改了引用,特此记录一下