SpringBoot--實現Mybatis的多數據源切換和動態數據源切換


環境依賴:  

  Spring Boot:1.5.9
  JDK:1.8.0
  MySQL:5.7.17
  Mybatis:3.3.0
 
  本文主要就mybatis的多數據源切換和動態數據源加載的實現原理做分享;對於mybatis的基礎可自行百度。由於在開始學習的時候,發現網上有很多人把多數據源切換和動態數據源加載混為一談,導致在實現動態加載的時候,所很苦惱。所以有必要在這里做以簡要說明:
  多數據源切換:指項目所需要不止一個數據庫的連接信息,eg:同一數據庫地址下的不同庫或者壓根連地址都不同。
  動態切換:指所需要的數據所在的數據庫信息在項目啟動前並不知道,只有在項目運行后根據業務邏輯獲取到對應的數據庫信息,並在代碼的運行過程中,向Spring Boot中添加一個或多個mybatis實例。
  

單一數據源的連接  

  顧名思義,在項目中,在項目中只需要配置一個數據庫的信息即可,業務所需要的所有數據均在這一個數據庫下;這種場景通常能夠適用於絕大部分的實際需求,因此這種實現的原理再次不做贅述,如有需求可自行百度。具體實現可參考源碼spring-boot-mybatis-demo。

多數據源切換  

  業務場景:需要分別獲取所有的用戶信息和學生信息;但是已知用戶信息在mybatis_demo數據庫中,學生信息在mybatis_demo2 數據庫中。如下圖所示:

 

數據庫mybatis_demo內有個用戶表:user_info,表結構如下:

 

數據庫mybatis_demo2內有一個學生表:student_info,表結構如下:

 

配置文件信息如下:

在這里介紹一種最為簡單的實現方案:多數據源 - 多實例。
在熟悉了單實例數據源的實現后,不難看出,在Spring Boot中,通過為該數據源DataSource初始化一個與之對應的SessionFactory,從而實現連接。因此在面對多數據源的時候,可以分別為每個數據源寫一個mybatis的config類,使其每個DataSource都擁有一個只屬於自己的SessionFactory,這樣就可以根據各自的mapper映射目錄找到對應的mybaits實例;
這種實現方法要求不同的mybatis實例的mapper映射目錄不能相同

 

把一個配置類作下的Bean命名統一,並注入相應的Bean,從而可以保證每一個SessionFactory所對應的配置信息唯一;具體配置如下:
第一個數據源的配置:

@Configuration
@MapperScan(basePackages = "com.yhyr.mybatis.mapper.UserMapper", sqlSessionTemplateRef = "oneSqlSessionTemplate")
public class MybatisConfig {

@Bean(name = "oneDataSource")
@ConfigurationProperties(prefix = "spring.datasource.one")
@Primary
public DataSource customDataSource() {
return DataSourceBuilder.create().build();
}

@Bean(name = "oneSqlSessionFactory")
@Primary
public SqlSessionFactory customSqlSessionFactory(@Qualifier("oneDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
return bean.getObject();
}

@Bean(name = "oneTransactionManager")
@Primary
public DataSourceTransactionManager customTransactionManager(@Qualifier("oneDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

@Bean(name = "oneSqlSessionTemplate")
@Primary
public SqlSessionTemplate customSqlSessionTemplate(@Qualifier("oneSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}

}

第二個數據源的配置:

@Configuration
@MapperScan(basePackages = "com.yhyr.mybatis.mapper.StudentMapper", sqlSessionTemplateRef = "anotherSqlSessionTemplate")
public class MybatisConfig2 {

@Bean(name = "anotherDataSource")
@ConfigurationProperties(prefix = "spring.datasource.another")
public DataSource customDataSource() {
return DataSourceBuilder.create().build();
}

@Bean(name = "anotherSqlSessionFactory")
public SqlSessionFactory customSqlSessionFactory(@Qualifier("anotherDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
return bean.getObject();
}

@Bean(name = "anotherTransactionManager")
public DataSourceTransactionManager customTransactionManager(@Qualifier("anotherDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

@Bean(name = "anotherSqlSessionTemplate")
public SqlSessionTemplate customSqlSessionTemplate(@Qualifier("anotherSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}

}

完成配置文件的配置后,可在工程目錄的mapper包下新建兩個目錄:UserMapper和StudentMapper,分別對應兩個數據源。這兩個目錄只能同級,或者不同目錄,不能互為子父目錄。

 

通過mapper接口和注解方式實現對數據的獲取,代碼如下:

@Select("SELECT * FROM user_info")
public interface UserInfoMapper {
  List<UserInfo> getUserInfo();
}
@Select("SELECT * FROM student_info")
public interface StudentInfoMapper {
    List<StudentInfo> getStudentInfo();
}

Service層的邏輯:分別注入UserInfoMapper 和 StudentInfoMapper,獲取用戶和學生信息;

@SpringBootApplication
public class MybatisApplication implements CommandLineRunner {
@Autowired
UserService userService;

@Autowired
StudentService studentService;

public static void main(String[] args) {
SpringApplication.run(MybatisApplication.class, args);
}

@Override
public void run(String... strings) {
List<UserInfo> userInfoList = userService.getUserInfo();
userInfoList.stream().forEach(userInfo -> System.out.println("name is : " + userInfo.getName() + "; sex is : " + userInfo.getSex() + "; age is : " + userInfo.getAge()));

List<StudentInfo> studentInfoList = studentService.getStudentInfo();
studentInfoList.stream().forEach(studentInfo -> System.out.println("studentName is : " + studentInfo.getStudentName() + "; className is : " + studentInfo.getClassName()));
}
}

最后貼上入口函數的邏輯和運行結果:

動態數據源切換

業務場景:
  現有已知的兩個數據源:default和master;
  default:用戶常規的業務邏輯,(eg:單數據源的業務需求)
  master:該數據源內只有一個db_info表,該表內維護這數據庫的基本信息(dbName, dbIp, dbPort, dbUser, dbPasswd)
  現在需要根據業務需求,獲取master中相應的數據庫基本信息,然后根據從獲取到的數據庫基本信息中獲取所需要的業務數據。(可類比Hadoop中的NameNode和DataNode的關系)

  在這種業務場景下,上述那種在程序執行前就一次性初始化所有mybatis實例的方法就行不通了。所以可以借助如下思路來思考:

  基於這種方式,不僅可是實現真正意義上的多數據源的切換(第二種實現多數據源切換的思路),還可以實現在程序的運行過程中,實現動態添加一個或多個新的數據源。這里重點關注的是配置文件之間的關系,對象模型如下:

首先分析一下AbstractRoutingDataSource抽象類的源碼:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;

public AbstractRoutingDataSource() {
}

public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}

public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}

public void setLenientFallback(boolean lenientFallback) {
this.lenientFallback = lenientFallback;
}

public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
}

public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
Iterator var1 = this.targetDataSources.entrySet().iterator();

while(var1.hasNext()) {
Entry<Object, Object> entry = (Entry)var1.next();
Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
this.resolvedDataSources.put(lookupKey, dataSource);
}

if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}

}
}

protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupKey;
}

protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource)dataSource;
} else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String)dataSource);
} else {
throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
}

public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username, password);
}

public <T> T unwrap(Class<T> iface) throws SQLException {
return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface);
}

public boolean isWrapperFor(Class<?> iface) throws SQLException {
return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
}

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;
}

if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}

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(),該方法用於指定到底需要使用哪一個數據源。

到此基本上清楚了該抽象類的使用方法,接下來貼下具體的實現代碼:
自定義數據源DataSource類:

public class DynamicDataSource extends AbstractRoutingDataSource {
private static DynamicDataSource instance;
private static byte[] lock=new byte[0];
private static Map<Object,Object> dataSourceMap=new HashMap<Object, Object>();

@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dataSourceMap.putAll(targetDataSources);
super.afterPropertiesSet();// 必須添加該句,否則新添加數據源無法識別到
}

public Map<Object, Object> getDataSourceMap() {
return dataSourceMap;
}

public static synchronized DynamicDataSource getInstance(){
if(instance==null){
synchronized (lock){
if(instance==null){
instance=new DynamicDataSource();
}
}
}
return instance;
}
//必須實現其方法
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDBType();
}
}

通過ThreadLocal維護一個全局唯一的map來實現數據源的動態切換

public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

public static synchronized void setDBType(String dbType){
contextHolder.set(dbType);
}

public static String getDBType(){
return contextHolder.get();
}

public static void clearDBType(){
contextHolder.remove();
}
}

Mybatis配置文件:

@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.default.url}")
private String defaultDBUrl;
@Value("${spring.datasource.default.username}")
private String defaultDBUser;
@Value("${spring.datasource.default.password}")
private String defaultDBPassword;
@Value("${spring.datasource.default.driver-class-name}")
private String defaultDBDreiverName;

@Value("${spring.datasource.master.url}")
private String masterDBUrl;
@Value("${spring.datasource.master.username}")
private String masterDBUser;
@Value("${spring.datasource.master.password}")
private String masterDBPassword;
@Value("${spring.datasource.default.driver-class-name}")
private String masterDBDreiverName;

@Bean
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();

DruidDataSource defaultDataSource = new DruidDataSource();
defaultDataSource.setUrl(defaultDBUrl);
defaultDataSource.setUsername(defaultDBUser);
defaultDataSource.setPassword(defaultDBPassword);
defaultDataSource.setDriverClassName(defaultDBDreiverName);

DruidDataSource masterDataSource = new DruidDataSource();
masterDataSource.setDriverClassName(masterDBDreiverName);
masterDataSource.setUrl(masterDBUrl);
masterDataSource.setUsername(masterDBUser);
masterDataSource.setPassword(masterDBPassword);

Map<Object,Object> map = new HashMap<>();
map.put("default", defaultDataSource);
map.put("master", masterDataSource);
dynamicDataSource.setTargetDataSources(map);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);

return dynamicDataSource;
}

@Bean
public SqlSessionFactory sqlSessionFactory(
@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/*.xml"));
return bean.getObject();

}

@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory)
throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}

其他業務邏輯同多數據源切換,下面貼上如何切換數據源:

@SpringBootApplication
public class DynamicApplication implements CommandLineRunner {
@Autowired
UserService userService;

@Autowired
DBService dbService;

@Autowired
StudentService studentService;

public static void main(String[] args) {
SpringApplication.run(DynamicApplication.class, args);
}

@Override
public void run(String... strings) throws Exception {
/**
* 獲取maste數據庫信息
*/
DataSourceContextHolder.setDBType("default");
List<UserInfo> userInfoList = userService.getUserInfo();
userInfoList.stream().forEach(userInfo -> System.out.println("name is : " + userInfo.getName() + "; sex is : " + userInfo.getSex() + "; age is : " + userInfo.getAge()));

/**
* 根據slave數據源獲取目標數據庫信息
*/
DataSourceContextHolder.setDBType("master");
int primayrId = 1;
DBInfo dbInfo = dbService.getDBInfoByprimayrId(primayrId);
System.out.println("dbName is -> " + dbInfo.getDbName() + "; dbIP is -> " + dbInfo.getDbIp() + "; dbUser is -> " + dbInfo.getDbUser() + "; dbPasswd is -> " + dbInfo.getDbPasswd());

DruidDataSource dynamicDataSource = new DruidDataSource();
dynamicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
dynamicDataSource.setUrl("jdbc:mysql://" + dbInfo.getDbIp() + ":" + dbInfo.getDbPort() + "/" + dbInfo.getDbName() + "?characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull");
dynamicDataSource.setUsername(dbInfo.getDbUser());
dynamicDataSource.setPassword(dbInfo.getDbPasswd());

/**
* 創建動態數據源
*/
Map<Object, Object> dataSourceMap = DynamicDataSource.getInstance().getDataSourceMap();
dataSourceMap.put("dynamic-slave", dynamicDataSource);
DynamicDataSource.getInstance().setTargetDataSources(dataSourceMap);
/**
* 切換為動態數據源實例,打印學生信息
*/
DataSourceContextHolder.setDBType("dynamic-slave");
List<StudentInfo> studentInfoList = studentService.getStudentInfo();
studentInfoList.stream().forEach(studentInfo -> System.out.println("studentName is : " + studentInfo.getStudentName() + "; className is : " + studentInfo.getClassName() + "; gradeName is : " + studentInfo.getGradeName()));

}
}

這種是在業務中使用代碼設置數據源的方式,也可以使用AOP+注解的方式實現控制,還可以前端頭部設置后端通過攔截器統一設置!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM