SpringBoot2+Mybatis多数据源切换和动态增减
MyBatis多数据源切换
项目结构为:
图片.png项目相关依赖pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
1、配置文件application.yml
编辑
spring:
datasource:
db1:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/cache?useSSL=FALSE&serverTimezone=UTC
username: root
password: root
db2:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/test?useSSL=FALSE&serverTimezone=UTC
username: root
password: root
# 这里不用配置mybatis的xml位置,在mybatis多数据源配置类中进行配置
#mybatis:
# mapper-locations:
# - classpath:mapper/db1/*.xml
# - classpath:mapper/db2/*.xml
2、创建枚举类DataSourceType
/**
* @author Hayson
* @description 列出所有数据源
*/
public enum DataSourceType {
db1,
db2
}
3、创建动态数据源上下文
/**
* @author Hayson
* @description 动态数据源上下文管理:设置数据源,获取数据源,清除数据源
*/
public class DataSourceContextHolder {
// 存放当前线程使用的数据源类型
private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
// 设置数据源
public static void setDataSource(DataSourceType type){
contextHolder.set(type);
}
// 获取数据源
public static DataSourceType getDataSource(){
return contextHolder.get();
}
// 清除数据源
public static void clearDataSource(){
contextHolder.remove();
}
}
4、动态数据源
/**
* @author Hayson
* @description 动态数据源,每执行一次数据库,动态获取数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
5、Mybatis多数据源配置
/**
* @author Hayson
* @description
*/
@Configuration
@MapperScan(basePackages = "com.example.multipledatabase.mapper")
public class MybatisConfig {
@Bean("db1DataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.db1")
public DataSource db1DataSource() {
DataSource build = DataSourceBuilder.create().build();
return build;
}
@Bean("db2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource db2DataSource(){
return DataSourceBuilder.create().build();
}
@Bean
public DynamicDataSource dataSource(@Qualifier("db1DataSource") DataSource db1DataSource,
@Qualifier("db2DataSource") DataSource db2DataSource) {
Map<Object, Object> map = new HashMap<>();
map.put(DataSourceType.db1, db1DataSource);
map.put(DataSourceType.db2, db2DataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(map);
dynamicDataSource.setDefaultTargetDataSource(db1DataSource);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dynamicDataSource);
// factoryBean.setTypeAliasesPackage();
// 设置mapper.xml的位置路径
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*/*.xml");
factoryBean.setMapperLocations(resources);
return factoryBean.getObject();
}
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource){
return new DataSourceTransactionManager(dynamicDataSource);
}
}
6、自定义注解
/*
* @author Hayson
* @description 自定义注解,用于类或方法上,优先级:方法>类
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceType value() default DataSourceType.db1;
}
7、AOP切面设置数据源
@Slf4j
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(ds)")
public void beforeDataSource(DataSource ds) {
DataSourceType value = ds.value();
DataSourceContextHolder.setDataSource(value);
log.info("当前使用的数据源为:{}", value);
}
@After("@annotation(ds)")
public void afterDataSource(DataSource ds){
DataSourceContextHolder.clearDataSource();
}
}
上面代码完后,即可以在Mybatis
的mapper
接口方法添加注解
@Repository
public interface GroupMapper {
@DataSource(value = DataSourceType.db2)
Map<String, Object> selectGroup();
}
或service
方法上添加注解:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final GroupMapper groupMapper;
public Map<String, Object> getUser(int id) {
return userMapper.selectUser(id);
}
@DataSource(value = DataSourceType.db2)
//@Transactional(rollbackFor = Exception.class) // 如果需要事务,可添加
public Map<String, Object> getUser2() {
return groupMapper.selectGroup();
}
}
上面的多数据源配置和切换已经完成,可实现在service
层或mapper
接口中添加注解@DataSource
指定使用数据源,并且能实现单数据源的事务回滚。
MyBatis运行期动态增减数据源
我们知道,在项目程序启动时,就会加载所有的配置文件信息,就会读取到配置文件中所有的数据源配置,像上面的多数据源,在启动时,就读取了两种数据源配置,在请求执行时,从两个数据源中选择指定一个去连接数据库。
而我目前负责的Bi项目中,就有数据库表中维护了所有客户的数据源,客户通过数据库的数据源连接到客户的数据库进行可视化数据分析。所以便想到通过在程序运行中,通过从数据库中获取数据源后,通过mybatis
进行数据查询,避免通过原生JDBC
进行查询,也方便SQL
的管理。
从上面多数据源配置切换中,知道需要继承AbstractRoutingDataSource
类,必须指定一个数据源:
简单分析一下AbstractRoutingDataSource
抽象类的部分源码:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;
... // 省略getter/setter
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);
}
}
}
... // 省略
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
...
}
@Nullable
protected abstract Object determineCurrentLookupKey();
}
对于该抽象类,关注两组变量:
-
Map<Object, Object> targetDataSources
和Object defaultTargetDataSource
-
Map<Object, DataSource> resolvedDataSources
和DataSource resolvedDefaultDataSource
这两组变量是相互对应的。在熟悉多实例数据源切换代码的不难发现,当有多个数据源的时候,一定要指定一个作为默认的数据源,所以,当同时初始化多个数据源的时候,需要调用setDefaultTargetDataSource
方法指定一个作为默认数据源;
我们需要关注的是Map<Object, Object> targetDataSources
和Map<Object, DataSource> resolvedDataSources
,targetDataSources
是暴露给外部程序用来赋值的,而resolvedDataSources
是程序内部执行时的依据,因此会有一个赋值的操作,如下图所示:
每次执行时,都会遍历targetDataSources
内的所有元素并赋值给resolvedDataSources
;这样如果我们在外部程序新增一个新的数据源,都会添加到内部使用,从而实现数据源的动态加载。
该抽象类有一个抽象方法:protected abstract Object determineCurrentLookupKey()
,该方法用于指定到底需要使用哪一个数据源:
了解上面两段源码后,可以进行多数据源切换代码改造:
-
修改
DynamicDataSource
:public class DynamicDataSource extends AbstractRoutingDataSource { private static Map<Object,Object> dataSourceMap=new HashMap<Object, Object>(); private static DynamicDataSource instance; private static byte[] lock=new byte[0]; // 重写setTargetDataSources,通过入参targetDataSources进行数据源的添加 @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { super.setTargetDataSources(targetDataSources); dataSourceMap.putAll(targetDataSources); super.afterPropertiesSet(); } // 单例模式,保证获取到都是同一个对象, public static synchronized DynamicDataSource getInstance(){ if(instance==null){ synchronized (lock){ if(instance==null){ instance=new DynamicDataSource(); } } } return instance; } @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } // 获取到原有的多数据源,并从该数据源基础上添加一个或多个数据源后, // 通过上面的setTargetDataSources进行加载 public Map<Object, Object> getDataSourceMap() { return dataSourceMap; } }
-
修改数据源类型枚举,之前是如下:
public enum DataSourceType { db1, db2 }
所以多数据源的配置类型指定为
DataSourceType
:-
图片DataSourceContextHolder
中
-
-
图片MyBatisDataSourceConfig
中 -
图片DataSourceAspect
中之前使用枚举类型进行配置,因为是固定了只有
db1
、db2
,所以可以统一指定了使用枚举类型,而现在进行动态添加数据源,因为从数据库获取到数据源,以该数据源的id
作为数据源的key
,所以统一使用String
类型的Key
。 -
修改枚举类
DataSourceType
:public enum DataSourceType { db1("db1"), db2("db2"); private String db; DataSourceType(String db) { this.db = db; } public String getDb() { return db; } public void setDb(String db) { this.db = db; } }
-
修改
DataSourceContextHolder
,将DataSourceType
改为String
public class DataSourceContextHolder { // 存放当前线程使用的数据源类型 private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); // 设置数据源 public static void setDataSource(String type){ contextHolder.set(type); } // 获取数据源 public static String getDataSource(){ return contextHolder.get(); } // 清除数据源 public static void clearDataSource(){ contextHolder.remove(); } }
-
修改
MyBatisDataSourceConfig
:@Configuration @MapperScan(basePackages = "com.example.multidatabase2.mapper") public class MyBatisDataSourceConfig { ... @Bean public DynamicDataSource dataSource(@Qualifier("db1DataSource") DataSource db1DataSource, @Qualifier("db2DataSource") DataSource db2DataSource) { Map<Object, Object> map = new HashMap<>(); // 添加的key为String类型 map.put(DataSourceType.db1.getDb(), db1DataSource); map.put(DataSourceType.db2.getDb(), db2DataSource); // 通过单例获取对象 DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance(); dynamicDataSource.setTargetDataSources(map); dynamicDataSource.setDefaultTargetDataSource(db1DataSource); return dynamicDataSource; } ...
-
修改
DataSourceAspect
public class DataSourceAspect { @Before("@annotation(ds)") public void beforeDataSource(DataSource ds) { // 修改为String String value = ds.value().getDb(); DataSourceContextHolder.setDataSource(value); log.info("当前使用的数据源为:{}", value); } ... }
测试:
@Service
public class StudentService {
//从db2中获取到drive-class、url、username、password信息
@DataSource(DataSourceType.db2)
public int test(String id, String username ){
// 通过id获取到drive-class、url、username、password
Map<String, Object> getdb = studentMapper.getdb(id);
// 配置数据源
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName((String) getdb.get("class_name"));
dataSource.setJdbcUrl((String)getdb.get("url"));
dataSource.setUsername((String)getdb.get("username"));
dataSource.setPassword((String)getdb.get("password"));
// 添加一个数据源到多数据源中
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
Map<Object, Object> dataSourceMap = dynamicDataSource.getDataSourceMap();
dataSourceMap.put(id, dataSource);
dynamicDataSource.setTargetDataSources(dataSourceMap);
// 切换数据源
DataSourceContextHolder.setDataSource(id);
// 获取用户信息
Map<String, Object> map = studentMapper.selectStudent(1);
// 更新id为1的用户信息
int i = updateStudent2(username, 1);
// 使用该数据源后,删除该数据源(如果不在使用)
DynamicDataSource instance = DynamicDataSource.getInstance();
Map<Object, Object> dataSourceMap = instance.getDataSourceMap();
dataSourceMap.remove(id);
instance.setTargetDataSources(dataSourceMap);
return i;
}
public int updateStudent2(String username, int id){
// 更新用户
int i = studentMapper.updateStudent(username, id);
return i;
}
}
}
上面就可以通过数据库获取信息进行配置数据源使用,使用后,可以删除。如果需要进行事务管理,可以把updateStudent2
方法放在另一个类中,加上注解@Transactional(rollbackFor = Exception.class)
即可。