最近、SaaSアプリケーションに取り組んでいます。データベースは、単一のインスタンスマルチスケマアーキテクチャを採用しています(詳細については、参照1を参照)。各テナントには独立したスキーマがあり、データソース全体に共有スキーマがあるため、動的な追加と削除、およびデータソースの切り替えの問題を解決する必要があります。
オンラインで多くの記事を検索した後、それらの多くはマスタースレーブデータソースの構成について話しているか、アプリケーションが開始される前にデータソースの構成を決定し、シャットダウンせずにデータソースを動的にロードする方法についてはめったに説明しないので、この記事を参照して書きました。
使用される手法
アイデア
リクエストが入ったら、現在のユーザーが属するテナントを決定し、テナント情報に基づいて対応するデータソースに切り替えてから、その後のビジネスオペレーションを実行します。
コード実装
TenantConfigentity(Tenant Information)@equalsandHashCode(calluper = false)@data@fielddefaults(revel = accesslevel.private)public class tenantconfigentity { / ***テナントid ** / integer tenantid; / ***テナント名**/文字列TenantName; / ***テナント名key **/ string tenantkey; / ***データベースurl **/ string dburl; / ***データベースユーザー名**/文字列dbuser; / ***データベースパスワード**/文字列dbpassword; / ***データベースpublic_key **/ string dbpublickey;} datasourceutil(aidantツールクラス、非必須)パブリッククラスDataSourceutil {private static final string data_source_bean_key_suffix = "_data_source"; private static final string jdbc_url_args = "?useunicode = true&charatereCoding = utf-8&useoldaliasmetadatabehavior = true&zerodateTimeBehavior = converttonull"; private static final string connection_properties = "config.decrypt = true; config.decrypt.key ="; / ** *スプライシングデータソースのスプリングビーンキー} tenantkey + data_source_bean_key_suffixを返します。 } / ** *スプライシング完全jdbc url * / public static string getjdbcurl(string baseurl){if(!stringutils.hastext(baseurl)){return null; } baseurl + jdbc_url_argsを返します。 } / ***スプライス完全なドルイド接続プロパティ* / public static string getConnectionProperties(string publicKey){if(!stringutils.hastext(publicKey)){return null; } connection_properties + publicKeyを返します。 }}DataSourceContextholder
ThreadLocalを使用して、現在のスレッドのデータソースキー名を保存し、セット、取得、およびクリアメソッドを実装します。
パブリッククラスDataSourceContextholder {private static final threadlocal <string> dataSourcekey = new EnersitableThreadLocal <>(); 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は、AbstractroutingDataSourceを拡張します{@Autowired Private ApplicationContext ApplicationContext; @lazy @autowired private dynamicdatasourcesummonerサモナー; @lazy @autowired private tenantconfigdao tenantconfigdao; @Override Protected String detienecurrentlookupkey(){string tenantkey = datasourcecontextholder.getDataSourcekey(); DataSourceutil.getDataSourceBeankey(TenantKey)を返します。 } @Override保護されたDataSource detienTargetDataSource(){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(); } super.determinetargetDataSource()を返します。 }}dynamicdatasourcesummoner(焦点のキーポイント)
データベースからデータソース情報をロードし、スプリングビーンを動的に組み立てて登録します。
@slf4j@componentpublic class dynamicdatasourcesummoner explments applicationlistener <contextrefreshedevent> {// spring-data-source.xmlのデフォルトデータソースIDと一致しています。 @autowired private configurableAbleapplicationContext ApplicationContext; @autowired private dynamicdatasource dynamicdatasource; @autowired private tenantconfigdao tenantconfigdao; Private static Booleanロード= false; / ***スプリングローディングが完了した後に実行*/ @Override public void onapplicationEvent(contextrefreshedevent event){//繰り返し実行を防止if(!loaded){loaded = true; try {RegisterDynamicDataSources(); } catch(Exception E){log.Error( "データソースの初期化に失敗した、例外:"、e); }}}}} / ***データベースからテナントのDB構成を読み取り、スプリングコンテナを動的に挿入します* / public void RegisterDynamicDataSources(){//すべてのテナントリスト<TenantConfigentity> TenantConfigentities = TenantConfigdao.listal(); if(collectionutils.isempty(tenantConfigentities)){新しいIllegalStateException( "アプリケーションの初期化に失敗した場合、データソースを最初に構成してください"); } //コンテナadddatasourcebeans(tenantconfigentities)にデータソースBeanを登録します。 } / *** dataSourceに基づいて豆を作成し、コンテナに登録します* / 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()); //データソースが春に登録されている場合、if(applicationContext.containsbean(beankey)){druiddatasource Execistsdatasource = applicationContext.getBean(beankey、druiddatasource.class); if(IssamedAtaSource(ExistsDataSource、Entity)){継続; }} // Bean AbstractBeanDefinition BeanDefinition = getBeanDefinition(Entity、Beankey)を組み立てる; // bean beanfactory.registerbeandefinition(beankey、beandefinition)を登録します。 //マップに入れて、Beanオブジェクトが作成されたことに注意してください。 } //作成されたマップオブジェクトをターゲットDataSourcesに設定します。 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(); } / ***スプリングコンテナのデータソースがデータベースのデータソース情報と一致しているかどうかを判断します*注:Public_keyについてはここには判断がありません。 objects.equals(existsdatasource.geturl()、datasourceutil.getjdbcurl(entity.getdburl())); if(!sameurl){return false; } boolean armyuser = 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( "データソースのパスワード検証に失敗した、例外:{}"、e); falseを返します。 }}}spring-data-source.xml
<! - jdbc構成ファイルの紹介 - > <コンテキスト:プロパティプレイスホルダーlocation = "classpath:data.properties" Ingrore-unresolvable = "true"/> <! - public(default)data source-> <bean id = "defaultdatasource" init-method = "init" init method = " value = "$ {ds.jdbcurl}" /> <プロパティname = "username" value = "$ {ds.user}" /> <property name = "password" value = "$ {ds.password}" /> <! name = "maxactive" value = "10" /> <! - ミリ秒単位でタイムアウトへの接続を待つ時間を構成 - > <プロパティname = "maxwait" value = "1000" /> <! - ミリ秒単位で閉じる必要があるアイドル接続を検出するのにかかる時間 - > <一度検出するには、ミリ秒単位で閉じる必要があるアイドル接続を検出します - > <プロパティ名= "timevetheevictionrunsmillis" value = "5000" /> <! - 1回検出するのにかかる時間を構成し、ミリ秒単位で閉じる必要があるアイドル接続を検出する時間 - > <> <プールで生き残るための接続の場合、ミリ秒単位で - > <プロパティ名= "minevictableidletimemillis" value = "240000" /> <プロパティ名= "validationquery" value = "select 1" /> <! - 秒:秒、接続が有効かどうかを検出するタイムアウト時間 - > <fallationqueriTimet " 安全。接続を申請する場合、アイドル時間がTimeEvenevictionRunsMillisよりも大きい場合、VeridationQueryを実行して、接続が有効かどうかを検出してください - > <プロパティ名= "testWhileIdle" value = "true" /> <! - [validationQuery]を実行して、接続を適用するときに接続が有効かどうかを検出します。この構成により、パフォーマンスが低下します。 - > <プロパティname = "testonborrow" value = "true" /> <! - 接続を返すときにvalidationQueryを実行して、接続が有効かどうかを確認します。この構成を行うと、パフォーマンスが低下します。 - > <プロパティ名= "testOnreturn" value = "false" /> <! - config filter-> <プロパティ名= "filters" value = "config" /> <propertyproperties "connectionproperties" value = "config.decrypt = true; config.decrypt.key = $ {ds.publickey}" /> < /bean> < name = "dataSource" ref = "multipledatasource"/> </bean> <! - multi-data source-> <bean id = "multipledatasource"> <プロパティ名= "defaulttargetdatasource" ref = "defaultdatasource"/> <プロパティname = "ターゲットダタスvalue-ref = "defaultdatasource"/> </map> </property> </bean> <! - annotationトランザクションマネージャー - > <! - ここでの注文値は、dynamicdatasourceaseppectadviceの注文値よりも大きい必要があります。 <bean id = "mainsqlsessionfactory"> <プロパティ名= "datasource" ref = "multipledatasource"/> </bean> <! - daoインターフェイスがあるパッケージ名、springは自動的にDAOを見つけます - > <bean id = "mainsqlmapper"> <プロパティ= "sqlsessionfactory name <プロパティ名= "basepackage" value = "abc*.dao"/> </bean> <bean id = "defaultsqlsessionfactory"> <プロパティ名= "datasource" ref = "defaultdatasource"/> </bean> <bean id = "defaultsqlmapper"> <プロパティ= " <プロパティ名= "basepackage" value = "abcbase.dal.dao"/> </bean> <! - その他の構成省略 - >dynamicdatasourceaspectadvice
参照のみを使用してAOPを使用してデータソースを自動的に切り替えます。
@Slf4j@Aspect@Component@Order(1) // Please note: the order here must be less than the order of tx:annotation-driven, that is, first execute the DynamicDataSourceAspectAdvice section, and then execute the transaction section to obtain the final data source @EnableAspectJAutoProxy(proxyTargetClass = true)public class DynamicDataSourceAspectAdvice { @Around( "execution(*abc*.controller。*。*(..)")public object doaround(proceedjoinpoint jp)throws slows {servletrequestattributes sra =(servletrequestattributes)requestcontextholder.getRequesttributes(); httpservletrequest request = sra.getRequest(); httpservletResponse応答= sra.getResponse(); string tenantkey = request.getheader( "Tenant"); //フロントエンドはテナントヘッダーに渡す必要があります。そうしないと、400が返されます(!stringutils.hastext(tenantkey)){webutils.tohttp(response).senderror(httpservletresponse.sc_bad_request); nullを返します。 } log.info( "現在のテナントキー:{}"、tenantkey); DataSourceContextholder.setDataSourcekey(TenantKey);オブジェクトresult = jp.proceed(); dataSourcecontextholder.clearDataSourcekey();返品結果; }}要約します
上記は、編集者によって導入された複数のデータソースのSpring動的登録の実装方法です。私はそれが誰にでも役立つことを願っています。ご質問がある場合は、メッセージを残してください。編集者は、すべての人に時間内に返信します。 wulin.comのウェブサイトへのご支援ありがとうございます!