最近在做SaaS應(yīng)用,數(shù)據(jù)庫采用了單實(shí)例多schema的架構(gòu)(詳見參考資料1),每個(gè)租戶有一個(gè)獨(dú)立的schema,同時(shí)整個(gè)數(shù)據(jù)源有一個(gè)共享的schema,因此需要解決動(dòng)態(tài)增刪、切換數(shù)據(jù)源的問題。
目前創(chuàng)新互聯(lián)已為上千余家的企業(yè)提供了網(wǎng)站建設(shè)、域名、虛擬空間、綿陽服務(wù)器托管、企業(yè)網(wǎng)站設(shè)計(jì)、孫吳網(wǎng)站維護(hù)等服務(wù),公司將堅(jiān)持客戶導(dǎo)向、應(yīng)用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。
在網(wǎng)上搜了很多文章后,很多都是講主從數(shù)據(jù)源配置,或都是在應(yīng)用啟動(dòng)前已經(jīng)確定好數(shù)據(jù)源配置的,甚少講在不停機(jī)的情況如何動(dòng)態(tài)加載數(shù)據(jù)源,所以寫下這篇文章,以供參考。
使用到的技術(shù)
思路
當(dāng)一個(gè)請(qǐng)求進(jìn)來的時(shí)候,判斷當(dāng)前用戶所屬租戶,并根據(jù)租戶信息切換至相應(yīng)數(shù)據(jù)源,然后進(jìn)行后續(xù)的業(yè)務(wù)操作。
代碼實(shí)現(xiàn)
TenantConfigEntity(租戶信息) @EqualsAndHashCode(callSuper = false) @Data @FieldDefaults(level = AccessLevel.PRIVATE) public class TenantConfigEntity { /** * 租戶id **/ Integer tenantId; /** * 租戶名稱 **/ String tenantName; /** * 租戶名稱key **/ String tenantKey; /** * 數(shù)據(jù)庫url **/ String dbUrl; /** * 數(shù)據(jù)庫用戶名 **/ String dbUser; /** * 數(shù)據(jù)庫密碼 **/ String dbPassword; /** * 數(shù)據(jù)庫public_key **/ String dbPublicKey; } DataSourceUtil(輔助工具類,非必要) public class DataSourceUtil { private static final String DATA_SOURCE_BEAN_KEY_SUFFIX = "_data_source"; private static final String JDBC_URL_ARGS = "?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&zeroDateTimeBehavior=convertToNull"; private static final String CONNECTION_PROPERTIES = "config.decrypt=true;config.decrypt.key="; /** * 拼接數(shù)據(jù)源的spring bean key */ public static String getDataSourceBeanKey(String tenantKey) { if (!StringUtils.hasText(tenantKey)) { return null; } return tenantKey + DATA_SOURCE_BEAN_KEY_SUFFIX; } /** * 拼接完整的JDBC URL */ public static String getJDBCUrl(String baseUrl) { if (!StringUtils.hasText(baseUrl)) { return null; } return baseUrl + JDBC_URL_ARGS; } /** * 拼接完整的Druid連接屬性 */ public static String getConnectionProperties(String publicKey) { if (!StringUtils.hasText(publicKey)) { return null; } return CONNECTION_PROPERTIES + publicKey; } }
DataSourceContextHolder
使用 ThreadLocal 保存當(dāng)前線程的數(shù)據(jù)源key name,并實(shí)現(xiàn)set、get、clear方法;
public class DataSourceContextHolder { private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal<>(); public static void setDataSourceKey(String tenantKey) { dataSourceKey.set(tenantKey); } public static String getDataSourceKey() { return dataSourceKey.get(); } public static void clearDataSourceKey() { dataSourceKey.remove(); } }
DynamicDataSource(重點(diǎn))
繼承 AbstractRoutingDataSource (建議閱讀其源碼,了解動(dòng)態(tài)切換數(shù)據(jù)源的過程),實(shí)現(xiàn)動(dòng)態(tài)選擇數(shù)據(jù)源;
public class DynamicDataSource extends AbstractRoutingDataSource { @Autowired private ApplicationContext applicationContext; @Lazy @Autowired private DynamicDataSourceSummoner summoner; @Lazy @Autowired private TenantConfigDAO tenantConfigDAO; @Override protected String determineCurrentLookupKey() { String tenantKey = DataSourceContextHolder.getDataSourceKey(); return DataSourceUtil.getDataSourceBeanKey(tenantKey); } @Override protected DataSource determineTargetDataSource() { String tenantKey = DataSourceContextHolder.getDataSourceKey(); String beanKey = DataSourceUtil.getDataSourceBeanKey(tenantKey); if (!StringUtils.hasText(tenantKey) || applicationContext.containsBean(beanKey)) { return super.determineTargetDataSource(); } if (tenantConfigDAO.exist(tenantKey)) { summoner.registerDynamicDataSources(); } return super.determineTargetDataSource(); } }
DynamicDataSourceSummoner(重點(diǎn)中的重點(diǎn))
從數(shù)據(jù)庫加載數(shù)據(jù)源信息,并動(dòng)態(tài)組裝和注冊spring bean,
@Slf4j @Component public class DynamicDataSourceSummoner implements ApplicationListener<ContextRefreshedEvent> { // 跟spring-data-source.xml的默認(rèn)數(shù)據(jù)源id保持一致 private static final String DEFAULT_DATA_SOURCE_BEAN_KEY = "defaultDataSource"; @Autowired private ConfigurableApplicationContext applicationContext; @Autowired private DynamicDataSource dynamicDataSource; @Autowired private TenantConfigDAO tenantConfigDAO; private static boolean loaded = false; /** * Spring加載完成后執(zhí)行 */ @Override public void onApplicationEvent(ContextRefreshedEvent event) { // 防止重復(fù)執(zhí)行 if (!loaded) { loaded = true; try { registerDynamicDataSources(); } catch (Exception e) { log.error("數(shù)據(jù)源初始化失敗, Exception:", e); } } } /** * 從數(shù)據(jù)庫讀取租戶的DB配置,并動(dòng)態(tài)注入Spring容器 */ public void registerDynamicDataSources() { // 獲取所有租戶的DB配置 List<TenantConfigEntity> tenantConfigEntities = tenantConfigDAO.listAll(); if (CollectionUtils.isEmpty(tenantConfigEntities)) { throw new IllegalStateException("應(yīng)用程序初始化失敗,請(qǐng)先配置數(shù)據(jù)源"); } // 把數(shù)據(jù)源bean注冊到容器中 addDataSourceBeans(tenantConfigEntities); } /** * 根據(jù)DataSource創(chuàng)建bean并注冊到容器中 */ private void addDataSourceBeans(List<TenantConfigEntity> tenantConfigEntities) { Map<Object, Object> targetDataSources = Maps.newLinkedHashMap(); DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); for (TenantConfigEntity entity : tenantConfigEntities) { String beanKey = DataSourceUtil.getDataSourceBeanKey(entity.getTenantKey()); // 如果該數(shù)據(jù)源已經(jīng)在spring里面注冊過,則不重新注冊 if (applicationContext.containsBean(beanKey)) { DruidDataSource existsDataSource = applicationContext.getBean(beanKey, DruidDataSource.class); if (isSameDataSource(existsDataSource, entity)) { continue; } } // 組裝bean AbstractBeanDefinition beanDefinition = getBeanDefinition(entity, beanKey); // 注冊bean beanFactory.registerBeanDefinition(beanKey, beanDefinition); // 放入map中,注意一定是剛才創(chuàng)建bean對(duì)象 targetDataSources.put(beanKey, applicationContext.getBean(beanKey)); } // 將創(chuàng)建的map對(duì)象set到 targetDataSources; dynamicDataSource.setTargetDataSources(targetDataSources); // 必須執(zhí)行此操作,才會(huì)重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有這樣,動(dòng)態(tài)切換才會(huì)起效 dynamicDataSource.afterPropertiesSet(); } /** * 組裝數(shù)據(jù)源spring bean */ private AbstractBeanDefinition getBeanDefinition(TenantConfigEntity entity, String beanKey) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(DruidDataSource.class); builder.getBeanDefinition().setAttribute("id", beanKey); // 其他配置繼承defaultDataSource builder.setParentName(DEFAULT_DATA_SOURCE_BEAN_KEY); builder.setInitMethodName("init"); builder.setDestroyMethodName("close"); builder.addPropertyValue("name", beanKey); builder.addPropertyValue("url", DataSourceUtil.getJDBCUrl(entity.getDbUrl())); builder.addPropertyValue("username", entity.getDbUser()); builder.addPropertyValue("password", entity.getDbPassword()); builder.addPropertyValue("connectionProperties", DataSourceUtil.getConnectionProperties(entity.getDbPublicKey())); return builder.getBeanDefinition(); } /** * 判斷Spring容器里面的DataSource與數(shù)據(jù)庫的DataSource信息是否一致 * 備注:這里沒有判斷public_key,因?yàn)榱硗馊齻€(gè)信息基本可以確定唯一了 */ private boolean isSameDataSource(DruidDataSource existsDataSource, TenantConfigEntity entity) { boolean sameUrl = Objects.equals(existsDataSource.getUrl(), DataSourceUtil.getJDBCUrl(entity.getDbUrl())); if (!sameUrl) { return false; } boolean sameUser = Objects.equals(existsDataSource.getUsername(), entity.getDbUser()); if (!sameUser) { return false; } try { String decryptPassword = ConfigTools.decrypt(entity.getDbPublicKey(), entity.getDbPassword()); return Objects.equals(existsDataSource.getPassword(), decryptPassword); } catch (Exception e) { log.error("數(shù)據(jù)源密碼校驗(yàn)失敗,Exception:{}", e); return false; } } }
spring-data-source.xml
<!-- 引入jdbc配置文件 --> <context:property-placeholder location="classpath:data.properties" ignore-unresolvable="true"/> <!-- 公共(默認(rèn))數(shù)據(jù)源 --> <bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 基本屬性 url、user、password --> <property name="url" value="${ds.jdbcUrl}" /> <property name="username" value="${ds.user}" /> <property name="password" value="${ds.password}" /> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="5" /> <property name="minIdle" value="2" /> <property name="maxActive" value="10" /> <!-- 配置獲取連接等待超時(shí)的時(shí)間,單位是毫秒 --> <property name="maxWait" value="1000" /> <!-- 配置間隔多久才進(jìn)行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="5000" /> <!-- 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="240000" /> <property name="validationQuery" value="SELECT 1" /> <!--單位:秒,檢測連接是否有效的超時(shí)時(shí)間--> <property name="validationQueryTimeout" value="60" /> <!--建議配置為true,不影響性能,并且保證安全性。申請(qǐng)連接的時(shí)候檢測,如果空閑時(shí)間大于timeBetweenEvictionRunsMillis,執(zhí)行validationQuery檢測連接是否有效--> <property name="testWhileIdle" value="true" /> <!--申請(qǐng)連接時(shí)執(zhí)行validationQuery檢測連接是否有效,做了這個(gè)配置會(huì)降低性能。--> <property name="testOnBorrow" value="true" /> <!--歸還連接時(shí)執(zhí)行validationQuery檢測連接是否有效,做了這個(gè)配置會(huì)降低性能。--> <property name="testOnReturn" value="false" /> <!--Config Filter--> <property name="filters" value="config" /> <property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${ds.publickey}" /> </bean> <!-- 事務(wù)管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="multipleDataSource"/> </bean> <!--多數(shù)據(jù)源--> <bean id="multipleDataSource" class="a.b.c.DynamicDataSource"> <property name="defaultTargetDataSource" ref="defaultDataSource"/> <property name="targetDataSources"> <map> <entry key="defaultDataSource" value-ref="defaultDataSource"/> </map> </property> </bean> <!-- 注解事務(wù)管理器 --> <!--這里的order值必須大于DynamicDataSourceAspectAdvice的order值--> <tx:annotation-driven transaction-manager="txManager" order="2"/> <!-- 創(chuàng)建SqlSessionFactory,同時(shí)指定數(shù)據(jù)源 --> <bean id="mainSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="multipleDataSource"/> </bean> <!-- DAO接口所在包名,Spring會(huì)自動(dòng)查找其下的DAO --> <bean id="mainSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="mainSqlSessionFactory"/> <property name="basePackage" value="a.b.c.*.dao"/> </bean> <bean id="defaultSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="defaultDataSource"/> </bean> <bean id="defaultSqlMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="defaultSqlSessionFactory"/> <property name="basePackage" value="a.b.c.base.dal.dao"/> </bean> <!-- 其他配置省略 -->
DynamicDataSourceAspectAdvice
利用AOP自動(dòng)切換數(shù)據(jù)源,僅供參考;
@Slf4j @Aspect @Component @Order(1) // 請(qǐng)注意:這里order一定要小于tx:annotation-driven的order,即先執(zhí)行DynamicDataSourceAspectAdvice切面,再執(zhí)行事務(wù)切面,才能獲取到最終的數(shù)據(jù)源 @EnableAspectJAutoProxy(proxyTargetClass = true) public class DynamicDataSourceAspectAdvice { @Around("execution(* a.b.c.*.controller.*.*(..))") public Object doAround(ProceedingJoinPoint jp) throws Throwable { ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = sra.getRequest(); HttpServletResponse response = sra.getResponse(); String tenantKey = request.getHeader("tenant"); // 前端必須傳入tenant header, 否則返回400 if (!StringUtils.hasText(tenantKey)) { WebUtils.toHttp(response).sendError(HttpServletResponse.SC_BAD_REQUEST); return null; } log.info("當(dāng)前租戶key:{}", tenantKey); DataSourceContextHolder.setDataSourceKey(tenantKey); Object result = jp.proceed(); DataSourceContextHolder.clearDataSourceKey(); return result; } }
總結(jié)
以上所述是小編給大家介紹的Spring動(dòng)態(tài)注冊多數(shù)據(jù)源的實(shí)現(xiàn)方法,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)創(chuàng)新互聯(lián)網(wǎng)站的支持!
新聞名稱:Spring動(dòng)態(tài)注冊多數(shù)據(jù)源的實(shí)現(xiàn)方法
本文鏈接:http://bm7419.com/article34/pdhcse.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供企業(yè)建站、全網(wǎng)營銷推廣、營銷型網(wǎng)站建設(shè)、網(wǎng)站導(dǎo)航、搜索引擎優(yōu)化、網(wǎng)站改版
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)