SpringBoot 多数据源简单实现
今天大致看了dynamic-datasource的源码,简单来说就是通过获取配置文件,加载自定义数据源,放入到Map中,当需要使用数据源时,通过getconnet()方法,从ThreadLocal的获取线程需要的数据源标识Key,在从Map中返回。
不过由于集成太多东西了,导致看的眼花缭乱的,根据自己的理解,简单实现下
首先是maven POM
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<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-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
</dependencies>
环境搭建好了,编写yml文件,具体怎么个格式,看个人喜欢,反正只要能获取到就可以了
server:
port: 8890
spring:
datasource:
custom:
configs:
- databaseName: master
url: jdbc:mysql://192.168.1.10:3306/travel?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
primary: true --默认数据库
- databaseName: slave
url: jdbc:mysql://192.168.1.10:3306/travel_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: TravelOrder
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.db.entity
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.order.mapper: debug
创建配置对应的model,
@Data
public class DatasourceProperties{
private String databaseName; //数据源名称
private String url; //连接url
private String username;
private String password;
private String driverClassName;
private Boolean primary = false;
private int initialSize;
private int minIdle;
private int maxActive;
private long maxWait;
}
然后通过**@ConfigurationProperties(prefix = “spring.datasource.custom”)**读取到configs集合中
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.custom")
public class DataSourceConfig{
private List<DatasourceProperties> configs;
public List<DatasourceProperties> getConfigs() {
return configs;
}
public void setConfigs(List<DatasourceProperties> configs) {
this.configs = configs;
}
}
既然文件已经读取进来了,就要开始实例化数据源,在DataSourceConfig中添加
@Bean
public DataSource dataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
// 根据实际情况返回当前的数据源标识
return DataSourceContextHolder.getDataSourceType();
}
};
//根据配置实例化全部的数据源
for (DatasourceProperties config : configs) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(config.getUrl());
dataSource.setUsername(config.getUsername());
dataSource.setPassword(config.getPassword());
dataSource.setDriverClassName(config.getDriverClassName());
targetDataSources.put(config.getDatabaseName(), dataSource);
if (config.getPrimary()){
routingDataSource.setDefaultTargetDataSource(dataSource); // 设置默认数据源
}
}
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
这里就是核心实现了,一步一步说首先是AbstractRoutingDataSource,这个类就是自定义获取数据源的关键,复制了一些关键的做下说明
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private Map<Object, Object> targetDataSources;
//获取连接
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//获取对应的key,这里是通过对抽象方法的实现,我们直接从ThreadLocal中获取
Object lookupKey = this.determineCurrentLookupKey();
//从map里面获取对应的数据源
DataSource 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 + "]");
} else {
return dataSource;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
//通过在DataSourceConfig,我们把全部的数据源对象都set了
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
//把targetDataSources赋值给resolvedDataSources,resolvedDataSources就是上面determineTargetDataSource()的map
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
}
所以现在有一个问题就是,有那么多线程,用什么方式把线程隔离开,并且每个线程都能获取到自己需要的数据源呢,所以用到了ThreadLocal(有可能涉及到多个方法多次切换,所以也可以用栈来替换String)
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
到这一步基本就可以了简单使用下
@Test
void contextLoads() throws SQLException {
DataSourceContextHolder.setDataSourceType("slave");
Stock stock = stockMapper.queryById(1);
}
返回
2024-06-26 20:22:02.327 INFO 20656 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
Stock(id=1, orderId=1, num=22)
基本完成了,后面就是通过Aop来简化使用了,直接对Mapper类进行Aop操作,这样就没必要每个方法都设置了
不过如果一个类里面切了多次数据源那么可以先把ThreadLocal里面的值清掉,在根据业务要求切换数据源
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DB {
String value() default "master";
}
@Configuration
@Aspect
public class DataSouceAop {
@Around("@within(db)")
public Object toChangeDataBase(ProceedingJoinPoint proceedingJoinPoint,DB db) throws Throwable {
String dbName = db.value();
DataSourceContextHolder.setDataSourceType(dbName);
Object proceed = proceedingJoinPoint.proceed();
DataSourceContextHolder.clearDataSourceType();
return proceed;
}
}
@DB("slave") //使用注解
public interface StockMapper {
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
Stock queryById(Integer id);
}
//默认
public interface TorderMapper {
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
Torder queryById(Integer id);
}
如果帮助到,点个赞-。-