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动态注册多数据源的实现方法,希望对大家有所帮助