在實際開發中,我們一個項目可能會用到多個數據庫,通常一個數據庫對應一個數據源。
代碼結構:

簡要原理:
1)DatabaseType列出所有的數據源的key---key
2)DatabaseContextHolder是一個線程安全的DatabaseType容器,並提供了向其中設置和獲取DatabaseType的方法
3)DynamicDataSource繼承AbstractRoutingDataSource並重寫其中的方法determineCurrentLookupKey(),在該方法中使用DatabaseContextHolder獲取當前線程的DatabaseType
4)MyBatisConfig中生成2個數據源DataSource的bean---value
5)MyBatisConfig中將1)和4)組成的key-value對寫入到DynamicDataSource動態數據源的targetDataSources屬性(當然,同時也會設置2個數據源其中的一個為DynamicDataSource的defaultTargetDataSource屬性中)
6)將DynamicDataSource作為primary數據源注入到SqlSessionFactory的dataSource屬性中去,並且該dataSource作為transactionManager的入參來構造DataSourceTransactionManager
7)使用的時候,在dao層或service層先使用DatabaseContextHolder設置將要使用的數據源key,然后再調用mapper層進行相應的操作,建議放在dao層去做(當然也可以使用spring aop+自定注解去做)
注意:在mapper層進行操作的時候,會先調用determineCurrentLookupKey()方法獲取一個數據源(獲取數據源:先根據設置去targetDataSources中去找,若沒有,則選擇defaultTargetDataSource),之后在進行數據庫操作。
1、假設有兩個數據庫,配置如下
application.properties
1 #the first datasource 2 jdbc.driverClassName = com.mysql.jdbc.Driver 3 jdbc.url = jdbc:mysql://xxx:3306/mytestdb?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8 4 jdbc.username = root 5 jdbc.password = 123 6 7 #the second datasource 8 jdbc2.driverClassName = com.mysql.jdbc.Driver 9 jdbc2.url = jdbc:mysql://xxx:3306/mytestdb2?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8 10 jdbc2.username = root 11 jdbc2.password = 123
說明:在之前的配置的基礎上,只增加了上述的第二個數據源。
2、DatabaseType
1 package com.xxx.firstboot.common.datasource; 2 3 /** 4 * 列出所有的數據源key(常用數據庫名稱來命名) 5 * 注意: 6 * 1)這里數據源與數據庫是一對一的 7 * 2)DatabaseType中的變量名稱就是數據庫的名稱 8 */ 9 public enum DatabaseType { 10 mytestdb,mytestdb2 11 }
作用:列舉數據源的key。
3、DatabaseContextHolder
1 package com.xxx.firstboot.common.datasource; 2 3 /** 4 * 作用: 5 * 1、保存一個線程安全的DatabaseType容器 6 */ 7 public class DatabaseContextHolder { 8 private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>(); 9 10 public static void setDatabaseType(DatabaseType type){ 11 contextHolder.set(type); 12 } 13 14 public static DatabaseType getDatabaseType(){ 15 return contextHolder.get(); 16 } 17 }
作用:構建一個DatabaseType容器,並提供了向其中設置和獲取DatabaseType的方法
4、DynamicDataSource
1 package com.xxx.firstboot.common.datasource; 2 3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 4 5 /** 6 * 動態數據源(需要繼承AbstractRoutingDataSource) 7 */ 8 public class DynamicDataSource extends AbstractRoutingDataSource { 9 protected Object determineCurrentLookupKey() { 10 return DatabaseContextHolder.getDatabaseType(); 11 } 12 }
作用:使用DatabaseContextHolder獲取當前線程的DatabaseType
5、MyBatisConfig
1 package com.xxx.firstboot.common; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 import java.util.Properties; 6 7 import javax.sql.DataSource; 8 9 import org.apache.ibatis.session.SqlSessionFactory; 10 import org.mybatis.spring.SqlSessionFactoryBean; 11 import org.mybatis.spring.annotation.MapperScan; 12 import org.springframework.beans.factory.annotation.Autowired; 13 import org.springframework.beans.factory.annotation.Qualifier; 14 import org.springframework.context.annotation.Bean; 15 import org.springframework.context.annotation.Configuration; 16 import org.springframework.context.annotation.Primary; 17 import org.springframework.core.env.Environment; 18 import org.springframework.core.io.support.PathMatchingResourcePatternResolver; 19 import org.springframework.jdbc.datasource.DataSourceTransactionManager; 20 21 import com.alibaba.druid.pool.DruidDataSourceFactory; 22 import com.xxx.firstboot.common.datasource.DatabaseType; 23 import com.xxx.firstboot.common.datasource.DynamicDataSource; 24 25 /** 26 * springboot集成mybatis的基本入口 1)創建數據源(如果采用的是默認的tomcat-jdbc數據源,則不需要) 27 * 2)創建SqlSessionFactory 3)配置事務管理器,除非需要使用事務,否則不用配置 28 */ 29 @Configuration // 該注解類似於spring配置文件 30 @MapperScan(basePackages = "com.xxx.firstboot.mapper") 31 public class MyBatisConfig { 32 33 @Autowired 34 private Environment env; 35 36 /** 37 * 創建數據源(數據源的名稱:方法名可以取為XXXDataSource(),XXX為數據庫名稱,該名稱也就是數據源的名稱) 38 */ 39 @Bean 40 public DataSource myTestDbDataSource() throws Exception { 41 Properties props = new Properties(); 42 props.put("driverClassName", env.getProperty("jdbc.driverClassName")); 43 props.put("url", env.getProperty("jdbc.url")); 44 props.put("username", env.getProperty("jdbc.username")); 45 props.put("password", env.getProperty("jdbc.password")); 46 return DruidDataSourceFactory.createDataSource(props); 47 } 48 49 @Bean 50 public DataSource myTestDb2DataSource() throws Exception { 51 Properties props = new Properties(); 52 props.put("driverClassName", env.getProperty("jdbc2.driverClassName")); 53 props.put("url", env.getProperty("jdbc2.url")); 54 props.put("username", env.getProperty("jdbc2.username")); 55 props.put("password", env.getProperty("jdbc2.password")); 56 return DruidDataSourceFactory.createDataSource(props); 57 } 58 59 /** 60 * @Primary 該注解表示在同一個接口有多個實現類可以注入的時候,默認選擇哪一個,而不是讓@autowire注解報錯 61 * @Qualifier 根據名稱進行注入,通常是在具有相同的多個類型的實例的一個注入(例如有多個DataSource類型的實例) 62 */ 63 @Bean 64 @Primary 65 public DynamicDataSource dataSource(@Qualifier("myTestDbDataSource") DataSource myTestDbDataSource, 66 @Qualifier("myTestDb2DataSource") DataSource myTestDb2DataSource) { 67 Map<Object, Object> targetDataSources = new HashMap<>(); 68 targetDataSources.put(DatabaseType.mytestdb, myTestDbDataSource); 69 targetDataSources.put(DatabaseType.mytestdb2, myTestDb2DataSource); 70 71 DynamicDataSource dataSource = new DynamicDataSource(); 72 dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法 73 dataSource.setDefaultTargetDataSource(myTestDbDataSource);// 默認的datasource設置為myTestDbDataSource 74 75 return dataSource; 76 } 77 78 /** 79 * 根據數據源創建SqlSessionFactory 80 */ 81 @Bean 82 public SqlSessionFactory sqlSessionFactory(DynamicDataSource ds) throws Exception { 83 SqlSessionFactoryBean fb = new SqlSessionFactoryBean(); 84 fb.setDataSource(ds);// 指定數據源(這個必須有,否則報錯) 85 // 下邊兩句僅僅用於*.xml文件,如果整個持久層操作不需要使用到xml文件的話(只用注解就可以搞定),則不加 86 fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包 87 fb.setMapperLocations( 88 new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));// 89 90 return fb.getObject(); 91 } 92 93 /** 94 * 配置事務管理器 95 */ 96 @Bean 97 public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception { 98 return new DataSourceTransactionManager(dataSource); 99 } 100 101 }
作用:
- 通過讀取application.properties文件生成兩個數據源(myTestDbDataSource、myTestDb2DataSource)
- 使用以上生成的兩個數據源構造動態數據源dataSource
- @Primary:指定在同一個接口有多個實現類可以注入的時候,默認選擇哪一個,而不是讓@Autowire注解報錯(一般用於多數據源的情況下)
- @Qualifier:指定名稱的注入,當一個接口有多個實現類的時候使用(在本例中,有兩個DataSource類型的實例,需要指定名稱注入)
- @Bean:生成的bean實例的名稱是方法名(例如上邊的@Qualifier注解中使用的名稱是前邊兩個數據源的方法名,而這兩個數據源也是使用@Bean注解進行注入的)
- 通過動態數據源構造SqlSessionFactory和事務管理器(如果不需要事務,后者可以去掉)
6、使用
ShopMapper:
1 package com.xxx.firstboot.mapper; 2 3 import org.apache.ibatis.annotations.Param; 4 import org.apache.ibatis.annotations.Result; 5 import org.apache.ibatis.annotations.Results; 6 import org.apache.ibatis.annotations.Select; 7 8 import com.xxx.firstboot.domain.Shop; 9 10 public interface ShopMapper { 11 12 @Select("SELECT * FROM t_shop WHERE id = #{id}") 13 @Results(value = { @Result(id = true, column = "id", property = "id"), 14 @Result(column = "shop_name", property = "shopName") }) 15 public Shop getShop(@Param("id") int id); 16 17 }
ShopDao:
1 package com.xxx.firstboot.dao; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.stereotype.Repository; 5 6 import com.xxx.firstboot.common.datasource.DatabaseContextHolder; 7 import com.xxx.firstboot.common.datasource.DatabaseType; 8 import com.xxx.firstboot.domain.Shop; 9 import com.xxx.firstboot.mapper.ShopMapper; 10 11 @Repository 12 public class ShopDao { 13 @Autowired 14 private ShopMapper mapper; 15 16 /** 17 * 獲取shop 18 */ 19 public Shop getShop(int id) { 20 DatabaseContextHolder.setDatabaseType(DatabaseType.mytestdb2); 21 return mapper.getShop(id); 22 } 23 }
注意:首先設置了數據源的key,然后調用mapper(在mapper中會首先根據該key從動態數據源中查詢出相應的數據源,之后取出連接進行數據庫操作)
ShopService:
1 package com.xxx.firstboot.service; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.stereotype.Service; 5 6 import com.xxx.firstboot.dao.ShopDao; 7 import com.xxx.firstboot.domain.Shop; 8 9 @Service 10 public class ShopService { 11 12 @Autowired 13 private ShopDao dao; 14 15 public Shop getShop(int id) { 16 return dao.getShop(id); 17 } 18 }
ShopController:
1 package com.xxx.firstboot.web; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RequestMethod; 6 import org.springframework.web.bind.annotation.RequestParam; 7 import org.springframework.web.bind.annotation.RestController; 8 9 import com.xxx.firstboot.domain.Shop; 10 import com.xxx.firstboot.service.ShopService; 11 12 import io.swagger.annotations.Api; 13 import io.swagger.annotations.ApiOperation; 14 15 @RestController 16 @RequestMapping("/shop") 17 @Api("shopController相關api") 18 public class ShopController { 19 20 @Autowired 21 private ShopService service; 22 23 @ApiOperation("獲取shop信息,測試多數據源") 24 @RequestMapping(value = "/getShop", method = RequestMethod.GET) 25 public Shop getShop(@RequestParam("id") int id) { 26 return service.getShop(id); 27 } 28 29 }
補:其實DatabaseContextHolder和DynamicDataSource完全可以合為一個類
參考:
http://www.cnblogs.com/lzrabbit/p/3750803.html
遺留:在實際開發中,一個dao類只會用到一個數據源,如果dao類中的方法很多的話,每一個方法前邊都要添加一個設置數據源的一句話,代碼有些冗余,可以使用AOP切面。
具體的實現方式見 第九章 springboot + mybatis + 多數據源 (AOP實現)
很多朋友反映遇到數據源循環依賴的問題,可以試一下將MyBatisConfig中的相關代碼換成這樣試試
1 @Bean 2 public SqlSessionFactory sqlSessionFactory(@Qualifier("myTestDbDataSource") DataSource myTestDbDataSource, 3 @Qualifier("myTestDb2DataSource") DataSource myTestDb2DataSource) throws Exception{ 4 SqlSessionFactoryBean fb = new SqlSessionFactoryBean(); 5 fb.setDataSource(this.dataSource(myTestDbDataSource, myTestDb2DataSource)); 6 fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage")); 7 fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations"))); 8 return fb.getObject(); 9 }
或者看看評論區已經解決了問題的朋友的方案。
