SpringBoot之多數據源動態切換數據源


原文:https://www.jianshu.com/p/cac4759b2684

 

實現

1、建庫建表

首先,我們在本地新建三個數據庫名分別為master,slave1,slave2,我們的目前就是寫入操作都是在master,查詢是 slave1,slave2
因此我們在上一篇也就是【SpringBoot2.0系列05】SpringBoot之整合Mybatis基礎上進行改動,
我們在master slave1 slave2中都創建user表 其中初始化salve1庫的user表數據為

 
image.png

初始化
slave2庫的 user
 
image.png

具體的數據庫腳本如下

 

create table master.user ( id bigint auto_increment comment '主鍵' primary key, age int null comment '年齡', password varchar(32) null comment '密碼', sex int null comment '性別', username varchar(32) null comment '用戶名' ) engine=MyISAM collate=utf8mb4_bin ; create table slave1.user ( id bigint auto_increment comment '主鍵' primary key, age int null comment '年齡', password varchar(32) null comment '密碼', sex int null comment '性別', username varchar(32) null comment '用戶名' ) engine=MyISAM collate=utf8mb4_bin ; INSERT INTO slave1.user (id, age, password, sex, username) VALUES (2, 22, 'admin', 1, 'admin'); create table slave2.user ( id bigint auto_increment comment '主鍵' primary key, age int null comment '年齡', password varchar(32) null comment '密碼', sex int null comment '性別', username varchar(32) null comment '用戶名' ) engine=MyISAM collate=utf8mb4_bin ; INSERT INTO slave2.user (id, age, password, sex, username) VALUES (3, 19, 'uuu', 2, 'user'); INSERT INTO slave2.user (id, age, password, sex, username) VALUES (4, 18, 'bbbb', 1, 'zzzz'); 

2、配置多數據源

經過上面初始化 我們的master.user是一張空表,我們等下的插入與更新操作就在這上面,那么我們的查詢操作就是在slave1.user跟slave2.user上面了。
上面我們的數據庫初始化工作完成了,接下來就是實現動態數據源的過程
首先我們需要在我們的application.yml配置我們的三個數據源

server: port: 8989 spring: datasource: master: password: root url: jdbc:mysql://127.0.0.1:3306/master?useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root type: com.zaxxer.hikari.HikariDataSource cluster: - key: slave1 password: root url: jdbc:mysql://127.0.0.1:3306/slave1?useUnicode=true&characterEncoding=UTF-8 idle-timeout: 20000 driver-class-name: com.mysql.jdbc.Driver username: root type: com.zaxxer.hikari.HikariDataSource - key: slave2 password: root url: jdbc:mysql://127.0.0.1:3306/slave2?useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root mybatis: mapper-locations: classpath:/mybatis/mapper/*.xml config-location: classpath:/mybatis/config/mybatis-config.xml 

在上面我們配置了三個數據,其中第一個作為默認數據源也就是我們的master數據源。主要是寫操作,那么讀操作交給我們的slave1跟slave2
其中 master 數據源是一定要配置 作為我們的默認數據源,其次cluster集群中,其他的數據不配置也不會影響程序的運行(相當於單數據源),如果你想添加新的一個數據源 就在cluster下新增一個數據源即可,其中key為必須項,用於數據源的唯一標識,以及接下來切換數據源的標識。

3、注冊數據源

在上面我們已經配置了三個數據源,但是這是我們自定義的配置,springboot是無法給我們自動配置,所以需要我們自己注冊數據源.
那么就要實現 EnvironmentAware用於讀取上下文環境變量用於構建數據源,同時也需要實現 ImportBeanDefinitionRegistrar接口注冊我們構建的數據源。com.yukong.chapter5.register.DynamicDataSourceRegister具體代碼如下

/** * 動態數據源注冊 * 實現 ImportBeanDefinitionRegistrar 實現數據源注冊 * 實現 EnvironmentAware 用於讀取application.yml配置 */ public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class); /** * 配置上下文(也可以理解為配置文件的獲取工具) */ private Environment evn; /** * 別名 */ private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); /** * 由於部分數據源配置不同,所以在此處添加別名,避免切換數據源出現某些參數無法注入的情況 */ static { aliases.addAliases("url", new String[]{"jdbc-url"}); aliases.addAliases("username", new String[]{"user"}); } /** * 存儲我們注冊的數據源 */ private Map<String, DataSource> customDataSources = new HashMap<String, DataSource>(); /** * 參數綁定工具 springboot2.0新推出 */ private Binder binder; /** * ImportBeanDefinitionRegistrar接口的實現方法,通過該方法可以按照自己的方式注冊bean * * @param annotationMetadata * @param beanDefinitionRegistry */ @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) { // 獲取所有數據源配置 Map config, defauleDataSourceProperties; defauleDataSourceProperties = binder.bind("spring.datasource.master", Map.class).get(); // 獲取數據源類型 String typeStr = evn.getProperty("spring.datasource.master.type"); // 獲取數據源類型 Class<? extends DataSource> clazz = getDataSourceType(typeStr); // 綁定默認數據源參數 也就是主數據源 DataSource consumerDatasource, defaultDatasource = bind(clazz, defauleDataSourceProperties); DynamicDataSourceContextHolder.dataSourceIds.add("master"); logger.info("注冊默認數據源成功"); // 獲取其他數據源配置 List<Map> configs = binder.bind("spring.datasource.cluster", Bindable.listOf(Map.class)).get(); // 遍歷從數據源 for (int i = 0; i < configs.size(); i++) { config = configs.get(i); clazz = getDataSourceType((String) config.get("type")); defauleDataSourceProperties = config; // 綁定參數 consumerDatasource = bind(clazz, defauleDataSourceProperties); // 獲取數據源的key,以便通過該key可以定位到數據源 String key = config.get("key").toString(); customDataSources.put(key, consumerDatasource); // 數據源上下文,用於管理數據源與記錄已經注冊的數據源key DynamicDataSourceContextHolder.dataSourceIds.add(key); logger.info("注冊數據源{}成功", key); } // bean定義類 GenericBeanDefinition define = new GenericBeanDefinition(); // 設置bean的類型,此處DynamicRoutingDataSource是繼承AbstractRoutingDataSource的實現類 define.setBeanClass(DynamicRoutingDataSource.class); // 需要注入的參數 MutablePropertyValues mpv = define.getPropertyValues(); // 添加默認數據源,避免key不存在的情況沒有數據源可用 mpv.add("defaultTargetDataSource", defaultDatasource); // 添加其他數據源 mpv.add("targetDataSources", customDataSources); // 將該bean注冊為datasource,不使用springboot自動生成的datasource beanDefinitionRegistry.registerBeanDefinition("datasource", define); logger.info("注冊數據源成功,一共注冊{}個數據源", customDataSources.keySet().size() + 1); } /** * 通過字符串獲取數據源class對象 * * @param typeStr * @return */ private Class<? extends DataSource> getDataSourceType(String typeStr) { Class<? extends DataSource> type; try { if (StringUtils.hasLength(typeStr)) { // 字符串不為空則通過反射獲取class對象 type = (Class<? extends DataSource>) Class.forName(typeStr); } else { // 默認為hikariCP數據源,與springboot默認數據源保持一致 type = HikariDataSource.class; } return type; } catch (Exception e) { throw new IllegalArgumentException("can not resolve class with type: " + typeStr); //無法通過反射獲取class對象的情況則拋出異常,該情況一般是寫錯了,所以此次拋出一個runtimeexception } } /** * 綁定參數,以下三個方法都是參考DataSourceBuilder的bind方法實現的,目的是盡量保證我們自己添加的數據源構造過程與springboot保持一致 * * @param result * @param properties */ private void bind(DataSource result, Map properties) { ConfigurationPropertySource source = new MapConfigurationPropertySource(properties); Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)}); // 將參數綁定到對象 binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result)); } private <T extends DataSource> T bind(Class<T> clazz, Map properties) { ConfigurationPropertySource source = new MapConfigurationPropertySource(properties); Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)}); // 通過類型綁定參數並獲得實例對象 return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get(); } /** * @param clazz * @param sourcePath 參數路徑,對應配置文件中的值,如: spring.datasource * @param <T> * @return */ private <T extends DataSource> T bind(Class<T> clazz, String sourcePath) { Map properties = binder.bind(sourcePath, Map.class).get(); return bind(clazz, properties); } /** * EnvironmentAware接口的實現方法,通過aware的方式注入,此處是environment對象 * * @param environment */ @Override public void setEnvironment(Environment environment) { logger.info("開始注冊數據源"); this.evn = environment; // 綁定配置器 binder = Binder.get(evn); } } 

上面代碼需要注意的是在springboot2.x系列中用於綁定的工具類如RelaxedPropertyResolver已經無法現在使用Binder代替。上面代碼主要是讀取application中數據源的配置,先讀取spring.datasource.master構建默認數據源,然后在構建cluster中的數據源。
在這里注冊完數據源之后,我們需要通過@import注解把我們的數據源注冊器導入到spring中 在啟動類Chapter5Application.java加上如下注解@Import(DynamicDataSourceRegister.class)
其中我們用到了一個DynamicDataSourceContextHolder 中的靜態變量來保存我們已經注冊成功的數據源的key,至此我們的數據源注冊就已經完成了。

4、配置數據源上下文

我們需要新建一個數據源上下文,用戶記錄當前線程使用的數據源的key是什么,以及記錄所有注冊成功的數據源的key的集合。對於線程級別的私有變量,我們首先ThreadLocal來實現。
com.yukong.chapter5.config.DynamicDataSourceContextHolder代碼取下

/** * @Auther: yukong * @Date: 2018/8/15 10:49 * @Description: 數據源上下文 */ public class DynamicDataSourceContextHolder { private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class); /** * 存儲已經注冊的數據源的key */ public static List<String> dataSourceIds = new ArrayList<>(); /** * 線程級別的私有變量 */ private static final ThreadLocal<String> HOLDER = new ThreadLocal<>(); public static String getDataSourceRouterKey () { return HOLDER.get(); } public static void setDataSourceRouterKey (String dataSourceRouterKey) { logger.info("切換至{}數據源", dataSourceRouterKey); HOLDER.set(dataSourceRouterKey); } /** * 設置數據源之前一定要先移除 */ public static void removeDataSourceRouterKey () { HOLDER.remove(); } /** * 判斷指定DataSrouce當前是否存在 * * @param dataSourceId * @return */ public static boolean containsDataSource(String dataSourceId){ return dataSourceIds.contains(dataSourceId); } } 

5、動態數據源路由

前面我們以及新建了數據源上下文,用於存儲我們當前線程的數據源key那么怎么通知spring用key當前的數據源呢,查閱資料可知,spring提供一個接口,名為AbstractRoutingDataSource的抽象類,我們只需要重寫determineCurrentLookupKey方法就可以,這個方法看名字就知道,就是返回當前線程的數據源的key,那我們只需要從我們剛剛的數據源上下文中取出我們的key即可,那么具體代碼取下。
com.yukong.chapter5.config.DynamicRoutingDataSource

/** * @Auther: yukong * @Date: 2018/8/15 10:47 * @Description: 動態數據源路由配置 */ public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private static Logger logger = LoggerFactory.getLogger(DynamicRoutingDataSource.class); @Override protected Object determineCurrentLookupKey() { String dataSourceName = DynamicDataSourceContextHolder.getDataSourceRouterKey(); logger.info("當前數據源是:{}", dataSourceName); return DynamicDataSourceContextHolder.getDataSourceRouterKey(); } } 

6、通過aop+注解實現動態數據源的切換

現在spring也已經知道通過key來取對應的數據源,我們現在只需要實現給對應的類或者方法設置他們的數據源的key,並且保存在數據源上下文中即可。這里我們采用注解來設置數據源,通過aop攔截並且保存到數據源上下中。
我們新建一個標識數據源的注解@DataSource具體代碼取下
com.yukong.chapter5.annotation.DataSource

/** * 切換數據注解 可以用於類或者方法級別 方法級別優先級 > 類級別 */ @Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String value() default "master"; //該值即key值 } 

其中他的默認值是master,因為我們默認數據源的key也是master。也就是說如果你直接用注解,而不指定value的話,那么默認就使用master默認數據源。
然后我們新建一個aop類來攔截。代碼如下
com.yukong.chapter5.aop

package com.yukong.chapter5.aop; import com.yukong.chapter5.annotation.DataSource; import com.yukong.chapter5.config.DynamicDataSourceContextHolder; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @Aspect @Component public class DynamicDataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class); @Before("@annotation(ds)") public void changeDataSource(JoinPoint point, DataSource ds) throws Throwable { String dsId = ds.value(); if (DynamicDataSourceContextHolder.dataSourceIds.contains(dsId)) { logger.debug("Use DataSource :{} >", dsId, point.getSignature()); } else { logger.info("數據源[{}]不存在,使用默認數據源 >{}", dsId, point.getSignature()); DynamicDataSourceContextHolder.setDataSourceRouterKey(dsId); } } @After("@annotation(ds)") public void restoreDataSource(JoinPoint point, DataSource ds) { logger.debug("Revert DataSource : " + ds.value() + " > " + point.getSignature()); DynamicDataSourceContextHolder.removeDataSourceRouterKey(); } } 

通過aop攔截,獲取注解上面的value的值key,然后取判斷我們注冊的keys集合中是否有這個key,如果沒有,則使用默認數據源,如果有,則設置上下文中當前數據源的key為注解的value。
7、測試
最后我們在對應的方法上面加上注解來測試一下即可
我們在UserMapper.java上面加上注解,並且進行測試。

/** * @Auther: yukong * @Date: 2018/8/13 19:47 * @Description: UserMapper接口 */ public interface UserMapper { /** * 新增用戶 * @param user * @return */ @DataSource //默認數據源 int save(User user); /** * 更新用戶信息 * @param user * @return */ @DataSource //默認數據源 int update(User user); /** * 根據id刪除 * @param id * @return */ @DataSource //默認數據源 int deleteById(Long id); /** * 根據id查詢 * @param id * @return */ @DataSource("slave1") //slave1 User selectById(Long id); /** * 查詢所有用戶信息 * @return */ @DataSource("slave2") //slave2 List<User> selectAll(); } 

上面代碼可以知道,我們的新增,修改,刪除方法都是在默認數據master上,我們的id查詢是在slave1,我們的查詢所有在slave2,我們編寫測試類來測試把。

/** * @Auther: yukong * @Date: 2018/8/14 16:34 * @Description: */ @SpringBootTest @RunWith(SpringJUnit4ClassRunner.class) public class UserMapperTest { @Autowired private UserMapper userMapper; @Test public void save() { User user = new User(); user.setUsername("master"); user.setPassword("master"); user.setSex(1); user.setAge(18); Assert.assertEquals(1,userMapper.save(user)); } @Test public void update() { User user = new User(); user.setId(8L); user.setPassword("newpassword"); // 返回插入的記錄數 ,期望是1條 如果實際不是一條則拋出異常 Assert.assertEquals(1,userMapper.update(user)); } @Test public void selectById() { User user = userMapper.selectById(2L); System.out.println("id:" + user.getId()); System.out.println("name:" + user.getUsername()); System.out.println("password:" + user.getPassword()); } @Test public void deleteById() { Assert.assertEquals(1,userMapper.deleteById(1L)); } @Test public void selectAll() { List<User> users= userMapper.selectAll(); users.forEach(user -> { System.out.println("id:" + user.getId()); System.out.println("name:" + user.getUsername()); System.out.println("password:" + user.getPassword()); }); } } 

首先測試save方法,它將會把數據存到master庫的user表,
現在user表是空的,如圖


 
image.png

運行save方法。


 
image.png

綠色,測試通過,並且日志提示數據源注冊成功,一共三個。並且當前使用的master數據源,我們再去master數據庫看看有沒有數據。

 
image.png

如上圖,插入成功。
新增方法測試完成了。我們在測試一下修改與刪除。
 
image.png

修改方法也測試通過,查看數據庫。
 
image.png

修改成功,刪除方法我就不測試, 我們在測試測試,slave1跟slave2數據源的方法,
首先測試 slave1的主鍵查詢方法,先看數據庫 slave1有哪些數據。
 
image.png

slave1.user就一條id為2 的數據並且id為2 的數據就slave1才有,我們測試一下能不能查到。
 
image.png

運行通過,數據源為 slave1並且數據也正確顯示。
最后我們來測試一下 slave2的selectAll方法把,同樣先看看 slave2.user中有什么數據。
 
image.png

從圖中,得知 slave2.user中有兩條數據,id分別為3,4。接下來運行測試方法。
結果如圖。
 
image.png

日志提示數據源切換值 slave2,並且id為3,4的數據也成功打印。
那么至此我們的 多數據源動態數據源就完成了。

 

主要的思路就是

  1. 配置文件中配置多個數據源
  2. 啟動類注冊動態數據源
  3. 在需要的方法上使用注解指定數據源

最后配套教程的代碼全部在這里
github https://github.com/YuKongEr/SpringBoot-Study。麻煩點個star或者fork吧。



作者:余空啊
鏈接:https://www.jianshu.com/p/cac4759b2684
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

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



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