欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring动态注册多数据源的实现方法

程序员文章站 2023-12-15 18:37:46
最近在做saas应用,数据库采用了单实例多schema的架构(详见参考资料1),每个租户有一个独立的schema,同时整个数据源有一个共享的schema,因此需要解决动态增...

最近在做saas应用,数据库采用了单实例多schema的架构(详见参考资料1),每个租户有一个独立的schema,同时整个数据源有一个共享的schema,因此需要解决动态增删、切换数据源的问题。

在网上搜了很多文章后,很多都是讲主从数据源配置,或都是在应用启动前已经确定好数据源配置的,甚少讲在不停机的情况如何动态加载数据源,所以写下这篇文章,以供参考。

使用到的技术

  • java8
  • spring + springmvc + mybatis
  • druid连接池
  • lombok
  • (以上技术并不影响思路实现,只是为了方便浏览以下代码片段)

思路

当一个请求进来的时候,判断当前用户所属租户,并根据租户信息切换至相应数据源,然后进行后续的业务操作。

代码实现

tenantconfigentity(租户信息)
@equalsandhashcode(callsuper = false)
@data
@fielddefaults(level = accesslevel.private)
public class tenantconfigentity {
 /**
  * 租户id
  **/
 integer tenantid;
 /**
  * 租户名称
  **/
 string tenantname;
 /**
  * 租户名称key
  **/
 string tenantkey;
 /**
  * 数据库url
  **/
 string dburl;
 /**
  * 数据库用户名
  **/
 string dbuser;
 /**
  * 数据库密码
  **/
 string dbpassword;
 /**
  * 数据库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=";
 /**
  * 拼接数据源的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 保存当前线程的数据源key name,并实现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(重点)

继承 abstractroutingdatasource (建议阅读其源码,了解动态切换数据源的过程),实现动态选择数据源;

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(重点中的重点)

从数据库加载数据源信息,并动态组装和注册spring bean,

@slf4j
@component
public class dynamicdatasourcesummoner implements applicationlistener<contextrefreshedevent> {
 // 跟spring-data-source.xml的默认数据源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加载完成后执行
  */
 @override
 public void onapplicationevent(contextrefreshedevent event) {
  // 防止重复执行
  if (!loaded) {
   loaded = true;
   try {
    registerdynamicdatasources();
   } catch (exception e) {
    log.error("数据源初始化失败, exception:", e);
   }
  }
 }
 /**
  * 从数据库读取租户的db配置,并动态注入spring容器
  */
 public void registerdynamicdatasources() {
  // 获取所有租户的db配置
  list<tenantconfigentity> tenantconfigentities = tenantconfigdao.listall();
  if (collectionutils.isempty(tenantconfigentities)) {
   throw new illegalstateexception("应用程序初始化失败,请先配置数据源");
  }
  // 把数据源bean注册到容器中
  adddatasourcebeans(tenantconfigentities);
 }
 /**
  * 根据datasource创建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());
   // 如果该数据源已经在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中,注意一定是刚才创建bean对象
   targetdatasources.put(beankey, applicationcontext.getbean(beankey));
  }
  // 将创建的map对象set到 targetdatasources;
  dynamicdatasource.settargetdatasources(targetdatasources);
  // 必须执行此操作,才会重新初始化abstractroutingdatasource 中的 resolveddatasources,也只有这样,动态切换才会起效
  dynamicdatasource.afterpropertiesset();
 }
 /**
  * 组装数据源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与数据库的datasource信息是否一致
  * 备注:这里没有判断public_key,因为另外三个信息基本可以确定唯一了
  */
 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("数据源密码校验失败,exception:{}", e);
   return false;
  }
 }
}

spring-data-source.xml

<!-- 引入jdbc配置文件 -->
 <context:property-placeholder location="classpath:data.properties" ignore-unresolvable="true"/>
 <!-- 公共(默认)数据源 -->
 <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" />
  <!-- 配置获取连接等待超时的时间,单位是毫秒 -->
  <property name="maxwait" value="1000" />
  <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
  <property name="timebetweenevictionrunsmillis" value="5000" />
  <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
  <property name="minevictableidletimemillis" value="240000" />
  <property name="validationquery" value="select 1" />
  <!--单位:秒,检测连接是否有效的超时时间-->
  <property name="validationquerytimeout" value="60" />
  <!--建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timebetweenevictionrunsmillis,执行validationquery检测连接是否有效-->
  <property name="testwhileidle" value="true" />
  <!--申请连接时执行validationquery检测连接是否有效,做了这个配置会降低性能。-->
  <property name="testonborrow" value="true" />
  <!--归还连接时执行validationquery检测连接是否有效,做了这个配置会降低性能。-->
  <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>
 <!-- 事务管理器 -->
 <bean id="txmanager" class="org.springframework.jdbc.datasource.datasourcetransactionmanager">
  <property name="datasource" ref="multipledatasource"/>
 </bean>
 <!--多数据源-->
 <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>
 <!-- 注解事务管理器 -->
 <!--这里的order值必须大于dynamicdatasourceaspectadvice的order值-->
 <tx:annotation-driven transaction-manager="txmanager" order="2"/>
 <!-- 创建sqlsessionfactory,同时指定数据源 -->
 <bean id="mainsqlsessionfactory" class="org.mybatis.spring.sqlsessionfactorybean">
  <property name="datasource" ref="multipledatasource"/>
 </bean>
 <!-- dao接口所在包名,spring会自动查找其下的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自动切换数据源,仅供参考;

@slf4j
@aspect
@component
@order(1) // 请注意:这里order一定要小于tx:annotation-driven的order,即先执行dynamicdatasourceaspectadvice切面,再执行事务切面,才能获取到最终的数据源
@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("当前租户key:{}", tenantkey);
  datasourcecontextholder.setdatasourcekey(tenantkey);
  object result = jp.proceed();
  datasourcecontextholder.cleardatasourcekey();
  return result;
 }
}

总结

以上所述是小编给大家介绍的spring动态注册多数据源的实现方法,希望对大家有所帮助

上一篇:

下一篇: