下面案例源码可参考:
- springboot-dydamic-datasources分包方式实现多数据源
- springboot-dydamic-datasources自定义注解实现多数据源
1.为什么微服务还要使用多数据源
:::tips 我们知道面对微服务化的拆分,我们希望一个微服务能做到小而精(做到只干一件事,比如账单服务,库存服务)。但是有些时候拆分不是很测地,需要在一个微服务项目中使用多数据源的情形(eg:如果满足a条件入a库,满足b条件入b库)。基于此,这里简单采用springboot分包方式实现多数据源。仅供参考。 :::2.分包方式实现多数据源切换
2.1 项目目录结构
我们在进行分包的方式来实现多数据源的动态切换,重点就是分包。项目分包如下:├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─itmck
│ │ │ ├─config 多数据源配置包
│ │ │ ├─dao
│ │ │ │ ├─master 主数据源mapper
│ │ │ │ └─slave 从数据源mapper
│ │ │ ├─entity 实体
│ │ │ └─service 接口
│ │ └─resources
│ │ └─mapper
│ │ ├─master 主数据源mapper.xml
│ │ └─slave 从数据源mapper.xml
│ └─test
2.2 数据源配置清单
```yaml spring: datasource: master: url: jdbc:mysql://localhost:3306/spring_db?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 slave: url: jdbc:mysql://localhost:3306/spring_db2?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 type: com.zaxxer.hikari.HikariDataSource hikari: connection-timeout: 30000 idle-timeout: 30000 auto-commit: ‘true’ minimum-idle: 5 maximum-pool-size: 15 pool-name: HikariCP connection-test-query: SELECT 1 FROM DUAL max-lifetime: 1800000
mybatis-plus: mapper-locations: ‘classpath:mapper//*Mapper.xml’ type-aliases-package: com.itmck.entity configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
<a name="PLVX3"></a>
## 2.3 master数据源
:::info
@MapperScan(basePackages = "com.itmck.dao.master", sqlSessionTemplateRef = "primarySqlSessionTemplate")<br />basePackages代表要扫描的mapper接口,这是分包切换数据源的关键
:::
```java
package com.itmck.filterconfig;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/17 15:07
**/
@Slf4j
@Configuration
@MapperScan(basePackages = "com.itmck.dao.master", sqlSessionTemplateRef = "primarySqlSessionTemplate")
public class PrimaryDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource primaryDateSource() {
return primaryDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
@Primary
public SqlSessionFactory primarySqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
mybatisSqlSessionFactoryBean.setPlugins(new PaginationInterceptor());
mybatisSqlSessionFactoryBean.setDataSource(primaryDateSource());
// mybatisSqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/master/*.xml"));
// mybatisSqlSessionFactoryBean.setConfiguration(this.mybatisConfiguration());//解决oracle下不能使用sequence问题
return mybatisSqlSessionFactoryBean.getObject();
}
private MybatisConfiguration mybatisConfiguration() {
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig().setKeyGenerator(new OracleKeyGenerator());
globalConfig.setDbConfig(dbConfig);
MybatisConfiguration mybatisConfiguration = new MybatisConfiguration();
mybatisConfiguration.setGlobalConfig(globalConfig);
return mybatisConfiguration;
}
@Bean
@Primary
public DataSourceTransactionManager primaryDataSourceTransactionManager() {
return new DataSourceTransactionManager(primaryDateSource());
}
@Bean
@Primary
public SqlSessionTemplate primarySqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(primarySqlSessionFactory());
}
}
2.4 slave数据源
:::info
@MapperScan(basePackages = “com.itmck.dao.slave”,sqlSessionTemplateRef = “slaveSqlSessionTemplate”)
basePackages代表要扫描的mapper接口,这是分包切换数据源的关键
:::
package com.itmck.filterconfig;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
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;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/17 15:07
* <p>
* 从数据源
**/
@Slf4j
@Configuration
@MapperScan(basePackages = "com.itmck.dao.slave",sqlSessionTemplateRef = "slaveSqlSessionTemplate")
public class SlaveDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSourceProperties slaveDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource slaveDateSource() {
return slaveDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
public SqlSessionFactory slaveSqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
mybatisSqlSessionFactoryBean.setPlugins(new PaginationInterceptor());
mybatisSqlSessionFactoryBean.setDataSource(slaveDateSource());
// mybatisSqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/slave/*.xml"));
// mybatisSqlSessionFactoryBean.setConfiguration(this.mybatisConfiguration());//解决oracle下不能使用sequence问题
return mybatisSqlSessionFactoryBean.getObject();
}
private MybatisConfiguration mybatisConfiguration() {
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig().setKeyGenerator(new OracleKeyGenerator());
globalConfig.setDbConfig(dbConfig);
MybatisConfiguration mybatisConfiguration = new MybatisConfiguration();
mybatisConfiguration.setGlobalConfig(globalConfig);
return mybatisConfiguration;
}
@Bean
public DataSourceTransactionManager slaveDataSourceTransactionManager() {
return new DataSourceTransactionManager(slaveDateSource());
}
@Bean
public SqlSessionTemplate slaveSqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(slaveSqlSessionFactory());
}
}
2.5 启动类 :::tips 启动类上exclude排除默认的数据源自动装配配置类。否则会出现无法确认数据源的错误。 :::
package com.itmck;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/17 15:03
**/
@SpringBootApplication(
//排除默认的数据源自动装配配置类
exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
MybatisPlusAutoConfiguration.class
}
)
@EnableTransactionManagement
public class SpringbootMultipartDataSourcesApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootMultipartDataSourcesApplication.class, args);
}
}
3.自定义注解方式实现多数据源
:::tips
使用多数据源也可以完成的.这里简单自定义注解实现多数据源切换.
场景:针对动态数据源的切换操作可以扩展成读写分离.举例:如果读写分离可以使用中间件mycat.
:::
3.1 项目目录结构
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─itmck
│ │ │ ├─annotation 自定义数据源切换注解
│ │ │ ├─config 切面以及配置类
│ │ │ ├─dao
│ │ │ ├─entity
│ │ │ ├─enums
│ │ │ ├─handler
│ │ │ └─service
│ │ └─resources
│ └─test
│ └─java
│ └─com
│ └─itmck
│ └─service
3.2 项目所需依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--这里引入aop目的自定义注解实现切面-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.3 application.yml配置
:::tips 注意: url改成 jdbc-url 否则报错: jdbcUrl is required with driverClassName 原文链接:解决方案 :::
server:
port: 8080
spring:
datasource:
master:
#这里url必须改为:jdbc-url 用来重写自定义连接池
jdbc-url: jdbc:mysql://localhost:3306/spring_db?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
slave:
jdbc-url: jdbc:mysql://localhost:3306/spring_db2?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
hikari:
connection-timeout: 30000
idle-timeout: 30000
auto-commit: 'true'
minimum-idle: 5
maximum-pool-size: 15
pool-name: HikariCP
connection-test-query: SELECT 1 FROM DUAL
max-lifetime: 1800000
mybatis-plus:
mapper-locations: 'classpath*:mapper/*/*Mapper.xml'
type-aliases-package: com.itmck.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.4 自定义注解
package com.itmck.annotation;
import com.itmck.enums.DataSourceType;
import java.lang.annotation.*;
/**
* 自定义多数据源切换注解
*
* @author miaochangke
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Ds
{
/**
* 切换数据源名称
*/
public DataSourceType value() default DataSourceType.MASTER;
}
package com.itmck.enums;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/22 14:28
* <p>
* 定义数据源枚举
**/
public enum DataSourceType {
/**
* 主库
*/
MASTER,
/**
* 从库
*/
SLAVE
}
3.5 自定义DynamicDataSourceContextHolder
:::tips 用来管理线程变量,根据线程进行数据源切换 :::
package com.itmck.handler;
import lombok.extern.slf4j.Slf4j;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/22 14:31
**/
@Slf4j
public class DynamicDataSourceContextHolder {
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源的变量
*/
public static void setDateSourceType(String dsType) {
log.info("切换到{}数据源", dsType);
CONTEXT_HOLDER.set(dsType);
}
/**
* 获得数据源的变量
*/
public static String getDateSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDateSourceType() {
CONTEXT_HOLDER.remove();
}
}
3.6 自定义DynamicDataSource
:::tips 通过自定义DynamicDataSource extends AbstractRoutingDataSource 重写determineCurrentLookupKey()来选择当前线程对应数据源 :::
package com.itmck.handler;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/22 14:37
* <p>
* 动态数据源
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);//设置默认数据源
super.setTargetDataSources(targetDataSources);//设置目标数据源
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDateSourceType();//选择数据源
}
}
3.7 创建DynamicDataSourceConfig
:::tips 初始化扫描数据源 :::
package com.itmck.filterconfig;
import com.itmck.enums.DataSourceType;
import com.itmck.handler.DynamicDataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.mybatis.spring.annotation.MapperScan;
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.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/22 14:43
**/
@Configuration
@MapperScan("com.itmck.dao")
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource primaryDateSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDateSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), primaryDateSource());
targetDataSources.put(DataSourceType.SLAVE.name(), slaveDateSource());
return new DynamicDataSource(primaryDateSource(), targetDataSources);
}
}
3.8 创建切面
使用切面针对使用了注解@Ds的方法进行数据源的切换
package com.itmck.filterconfig;
import com.itmck.annotation.Ds;
import com.itmck.handler.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/22 14:39
**/
@Slf4j
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.itmck.annotation.Ds)")
public void dsPointCut() {
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Ds ds = method.getAnnotation(Ds.class);
if (null != ds) {
DynamicDataSourceContextHolder.setDateSourceType(ds.value().name());
}
try {
return point.proceed();
} finally {
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDateSourceType();
}
}
}
3.9 注解使用
:::tips 参考DsChangeImpl#getStudentList() 切换数据源: @Ds(value = DataSourceType.SLAVE) :::
package com.itmck.service;
import com.baomidou.mybatisplus.extension.service.additional.query.impl.LambdaQueryChainWrapper;
import com.itmck.annotation.Ds;
import com.itmck.dao.StudentMapper;
import com.itmck.dao.UserMapper;
import com.itmck.entity.Student;
import com.itmck.entity.User;
import com.itmck.enums.DataSourceType;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/17 15:49
**/
@Service
public class DsChangeImpl implements DsChange {
@Resource
private UserMapper userMapper;
@Resource
private StudentMapper studentMapper;
//使用默认数据源
@Override
public List<User> getUserList() {
return new LambdaQueryChainWrapper<>(userMapper).list();
}
//使用从数据源
@Override
@Ds(value = DataSourceType.SLAVE)
public List<Student> getStudentList() {
return new LambdaQueryChainWrapper<>(studentMapper).list();
}
}
3.10 启动类排除数据源自动注册
:::tips @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 排除数据源自动配置类 :::
package com.itmck;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/17 15:03
**/
//@Import(DynamicDataSourceConfig.class)
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
public class SpringbootDynamicDataSourcesApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDynamicDataSourcesApplication.class, args);
}
}
3.11 测试
package com.itmck.service;
import com.itmck.entity.Student;
import com.itmck.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.List;
/**
* 太阳当空照,花儿对我笑
* <p>
* Create by M ChangKe 2021/11/17 15:49
**/
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DsChangeImplTest {
@Resource
private DsChange dsChange;
@Test
public void userMapper() {
List<User> list = dsChange.getUserList();
log.info("list:{}", list);
}
@Test
public void studentMapper() {
List<Student> list = dsChange.getStudentList();
log.info("list:{}", list);
}
}
分别输出:
==> Preparing: SELECT id,name,email,age FROM user ==> Parameters: <== Columns: id, name, email, age <== Row: 1, mck, 1735580535, 26 <== Total: 1 list:[User(id=1, name=mck, age=26, email=1735580535)]
==> Preparing: SELECT id,name,age FROM student ==> Parameters: <== Columns: id, name, age <== Row: 1, wxp, 25 <== Total: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@51b01550] list:[Student(id=1, name=wxp, age=25)]