Spring動(dòng)態(tài)注冊(cè)多數(shù)據(jù)源的實(shí)現(xiàn)方法
最近在做SaaS應(yīng)用,數(shù)據(jù)庫(kù)采用了單實(shí)例多schema的架構(gòu)(詳見(jiàn)參考資料1),每個(gè)租戶(hù)有一個(gè)獨(dú)立的schema,同時(shí)整個(gè)數(shù)據(jù)源有一個(gè)共享的schema,因此需要解決動(dòng)態(tài)增刪、切換數(shù)據(jù)源的問(wèn)題。
在網(wǎng)上搜了很多文章后,很多都是講主從數(shù)據(jù)源配置,或都是在應(yīng)用啟動(dòng)前已經(jīng)確定好數(shù)據(jù)源配置的,甚少講在不停機(jī)的情況如何動(dòng)態(tài)加載數(shù)據(jù)源,所以寫(xiě)下這篇文章,以供參考。
使用到的技術(shù)
- Java8
- Spring + SpringMVC + MyBatis
- Druid連接池
- Lombok
- (以上技術(shù)并不影響思路實(shí)現(xiàn),只是為了方便瀏覽以下代碼片段)
思路
當(dāng)一個(gè)請(qǐng)求進(jìn)來(lái)的時(shí)候,判斷當(dāng)前用戶(hù)所屬租戶(hù),并根據(jù)租戶(hù)信息切換至相應(yīng)數(shù)據(jù)源,然后進(jìn)行后續(xù)的業(yè)務(wù)操作。
代碼實(shí)現(xiàn)
TenantConfigEntity(租戶(hù)信息)
@EqualsAndHashCode(callSuper = false)
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TenantConfigEntity {
/**
* 租戶(hù)id
**/
Integer tenantId;
/**
* 租戶(hù)名稱(chēng)
**/
String tenantName;
/**
* 租戶(hù)名稱(chēng)key
**/
String tenantKey;
/**
* 數(shù)據(jù)庫(kù)url
**/
String dbUrl;
/**
* 數(shù)據(jù)庫(kù)用戶(hù)名
**/
String dbUser;
/**
* 數(shù)據(jù)庫(kù)密碼
**/
String dbPassword;
/**
* 數(shù)據(jù)庫(kù)public_key
**/
String dbPublicKey;
}
DataSourceUtil(輔助工具類(lèi),非必要)
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ù)源的過(guò)程),實(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ù)庫(kù)加載數(shù)據(jù)源信息,并動(dòng)態(tài)組裝和注冊(cè)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ù)庫(kù)讀取租戶(hù)的DB配置,并動(dòng)態(tài)注入Spring容器
*/
public void registerDynamicDataSources() {
// 獲取所有租戶(hù)的DB配置
List<TenantConfigEntity> tenantConfigEntities = tenantConfigDAO.listAll();
if (CollectionUtils.isEmpty(tenantConfigEntities)) {
throw new IllegalStateException("應(yīng)用程序初始化失敗,請(qǐng)先配置數(shù)據(jù)源");
}
// 把數(shù)據(jù)源bean注冊(cè)到容器中
addDataSourceBeans(tenantConfigEntities);
}
/**
* 根據(jù)DataSource創(chuàng)建bean并注冊(cè)到容器中
*/
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里面注冊(cè)過(guò),則不重新注冊(cè)
if (applicationContext.containsBean(beanKey)) {
DruidDataSource existsDataSource = applicationContext.getBean(beanKey, DruidDataSource.class);
if (isSameDataSource(existsDataSource, entity)) {
continue;
}
}
// 組裝bean
AbstractBeanDefinition beanDefinition = getBeanDefinition(entity, beanKey);
// 注冊(cè)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ù)庫(kù)的DataSource信息是否一致
* 備注:這里沒(méi)有判斷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)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="5000" />
<!-- 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="240000" />
<property name="validationQuery" value="SELECT 1" />
<!--單位:秒,檢測(cè)連接是否有效的超時(shí)時(shí)間-->
<property name="validationQueryTimeout" value="60" />
<!--建議配置為true,不影響性能,并且保證安全性。申請(qǐng)連接的時(shí)候檢測(cè),如果空閑時(shí)間大于timeBetweenEvictionRunsMillis,執(zhí)行validationQuery檢測(cè)連接是否有效-->
<property name="testWhileIdle" value="true" />
<!--申請(qǐng)連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效,做了這個(gè)配置會(huì)降低性能。-->
<property name="testOnBorrow" value="true" />
<!--歸還連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效,做了這個(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)前租戶(hù)key:{}", tenantKey);
DataSourceContextHolder.setDataSourceKey(tenantKey);
Object result = jp.proceed();
DataSourceContextHolder.clearDataSourceKey();
return result;
}
}
總結(jié)
以上所述是小編給大家介紹的Spring動(dòng)態(tài)注冊(cè)多數(shù)據(jù)源的實(shí)現(xiàn)方法,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
Java中StringBuilder字符串類(lèi)型的操作方法及API整理
Java中的StringBuffer類(lèi)繼承于AbstractStringBuilder,用來(lái)創(chuàng)建非線程安全的字符串類(lèi)型對(duì)象,下面即是對(duì)Java中StringBuilder字符串類(lèi)型的操作方法及API整理2016-05-05
java進(jìn)行error捕獲和處理示例(java異常捕獲)
通常來(lái)說(shuō),大家都是對(duì)Java中的Exception進(jìn)行捕獲和進(jìn)行相應(yīng)的處理,有些人說(shuō),error就無(wú)法捕獲了。其實(shí),error也是可以捕獲的。Error和Exception都是Throwable的子類(lèi)。既然可以catch Throwable,那么error也是可以catch的2014-01-01
maven實(shí)現(xiàn)jar包導(dǎo)入+導(dǎo)出方式
這篇文章主要介紹了maven實(shí)現(xiàn)jar包導(dǎo)入+導(dǎo)出方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Java如何實(shí)現(xiàn)可折疊Panel方法示例
這篇文章主要給大家介紹了關(guān)于利用Java如何實(shí)現(xiàn)可折疊Panel的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
Springboot 整合 Dubbo/ZooKeeper 實(shí)現(xiàn) SOA 案例解析
這篇文章主要介紹了Springboot 整合 Dubbo/ZooKeeper 詳解 SOA 案例,需要的朋友可以參考下2017-11-11
Java實(shí)現(xiàn)字符串的分割(基于String.split()方法)
Java中的我們可以利用split把字符串按照指定的分割符進(jìn)行分割,然后返回字符串?dāng)?shù)組,下面這篇文章主要給大家介紹了關(guān)于Java實(shí)現(xiàn)字符串的分割的相關(guān)資料,是基于jDK1.8版本中的String.split()方法,需要的朋友可以參考下2022-09-09
Java對(duì)List進(jìn)行排序的方法總結(jié)
在Java中,對(duì)List進(jìn)行排序是一項(xiàng)常見(jiàn)的任務(wù),Java提供了多種方法來(lái)對(duì)List中的元素進(jìn)行排序,本文將詳細(xì)介紹如何使用Java來(lái)實(shí)現(xiàn)List的排序操作,涵蓋了常用的排序方法和技巧,需要的朋友可以參考下2024-07-07

