Récemment, nous travaillons sur les applications SaaS. La base de données adopte une architecture multi-schema d'instance unique (voir référence 1 pour plus de détails). Chaque locataire a un schéma indépendant, et toute la source de données a un schéma partagé, il est donc nécessaire de résoudre le problème de l'addition dynamique et de la suppression et de la commutation de sources de données.
Après avoir recherché de nombreux articles en ligne, beaucoup d'entre eux parlent de la configuration de la source de données-esclave, ou ils ont déterminé la configuration de la source de données avant le démarrage de l'application, et parlent rarement de la façon de charger dynamiquement la source de données sans arrêter, j'ai donc écrit cet article pour référence.
Techniques utilisées
Idées
Lorsqu'une demande arrive, déterminez le locataire auquel appartient l'utilisateur actuel et passez à la source de données correspondante en fonction des informations du locataire, puis effectuez des opérations commerciales ultérieures.
Implémentation de code
TenantConfigentity (Informations sur les locataires) @EqualsandHashCode (CallesUper = False) @ data @ fieldDefaults (niveau = accessoire.private) Classe publique TenantConfigentity {/ ** * Tenant ID ** / Integer Tenantid; / ** * Nom du locataire ** / String locantName; / ** * Nom du locataire Key ** / String TenantKey; / ** * URL de la base de données ** / String dburl; / ** * Nom d'utilisateur de la base de données ** / String dbuser; / ** * Mot de passe de la base de données ** / String dbpassword; / ** * Database Public_Key ** / String dbpublicKey;} dataSourceUtil (classe d'outils aidants, non essentiel) classe publique DataSourceUtil {String final static private data_source_bean_key_suffix = "_data_source"; chaîne finale statique privée jdbc_url_args = "? useunicode = true & caractèrescoding = utf-8 & useoldaliasmetadatabehavior = true & zerodatetimebehavior = converttonull"; Connexion de chaîne finale statique privée_properties = "config.decrypt = true; config.decrypt.key ="; / ** * Spring Bean Key pour l'épissage de sources de données * / public static String getDataSourceBeanKey (String TenantKey) {if (! StringUtils.hastext (TenantKey)) {return null; } return locantkey + data_source_bean_key_suffix; } / ** * Splicing complete jdbc URL * / public static String getJdbcurl (String bunterl) {if (! StringUtils.hastext (basicUrl)) {return null; } return BUSURL + JDBC_URL_ARGS; } / ** * Propriétés de connexion Druid complètes épissées * / public static String getConnectionProperties (String publicKey) {if (! StringUtils.hastext (publicKey)) {return null; } return connection_properties + publicKey; }}Datasourcecontextholder
Utilisez ThreadLocal pour enregistrer le nom de la clé de source de données du thread actuel et implémentez les méthodes SET, GET et CLEAR;
classe publique DataSourCeContexTholder {private static final ThreadLocal <string> dataSourceKey = new hheRitableThreadLocal <> (); public static void setDataSourceKey (String TenantKey) {dataSourceKey.set (TenantKey); } public static static getDataSourceKey () {return dataSourceKey.get (); } public static void clearDataSourceKey () {dataSourceKey.Remove (); }}DynamicDatasource (point clé)
Hériter d'abstractroutingDataSource (il est recommandé de lire son code source pour comprendre le processus de commutation dynamiquement des sources de données) et de réaliser la sélection dynamique des sources de données;
La classe publique DynamicDataSource étend AbstratTroutingDataSource {@autowired ApplicationContext ApplicationContext; @Lazy @autowired private dynamicdatasourcesummoner invoconer; @Lazy @Autowired Private TenantConfigdao TenantConfigdao; @Override Protected String déterminantEcurrentLookUpKey () {String locantKey = dataSourceContexTholder.getDataSourceKey (); return dataSourceutil.getDataSourceBeanKey (TenantKey); } @Override Protected DataSource DeterminetargetDataSource () {String locantKey = dataSourceContexTholder.getDataSourceKey (); String beankey = dataSourceUtil.getDataSourceBeanKey (TenantKey); if (! StringUtils.hastext (TenantKey) || ApplicationContext.ContainsBean (Beankey)) {return super.DetMinetargetDataSource (); } if (TenantConfigDao.Exist (TenantKey)) {Summoner.RegisterDamicDataSources (); } return super.DetMinetargetDataSource (); }}Dynamicdatasourcesummoner (point clé de mise au point)
Chargez les informations de source de données à partir de la base de données et assemblez et enregistrez dynamiquement les haricots à ressort.
@ Slf4j @ ComponentPublic Class DynamicDataSourcesUmMoner implémente ApplicationListener <ContextreFreshEDEvent> {// cohérent avec l'ID de source de données par défaut de Spring-data-source.xml Private Static String final default_data_source_bean_key = "DefaultDataSource"; @Autowired private configurableApplicationContext applicationContext; @Autowired Private DynamicDataSource DynamicDataSource; @Autowired Private TenantConfigdao TenantConfigdao; Boolean statique privé chargé = false; / ** * Exécuter une fois le chargement de ressort terminé * / @Override public void onApplicationEvent (contextreFreshEdEvent event) {// empêcher l'exécution répétée if (! Chargé) {chargé = true; essayez {registredynamicDataSources (); } catch (exception e) {log.Error ("L'initialisation de la source de données a échoué, exception:", e); }}}} / ** * Lisez la configuration DB du locataire à partir de la base de données et injectez dynamiquement le conteneur de printemps * / public void RegisterDamicDataSources () {// Obtenez la configuration DB pour tous les locataires <TenantConfigentity> TenantConfigentities = TenantConfigda.Listall (); if (CollectionUtils.Isempty (TenantConfigentities)) {Throw New illégalStateException ("L'initialisation de l'application a échoué, veuillez d'abord configurer la source de données"); } // Enregistrez le bean de source de données dans le conteneur addDataSourceBeans (locataireconfigertities); } / ** * Créer des beans basés sur DataSource et vous inscrire dans le conteneur * / private void addDataSourceBeans (list <locantConfigentity> locantConfigentities) {map <object, object> cibleDataSources = maps.newLinkedHashMap (); DefaultListableBeAnfactory Beanfactory = (DefaultListableBeAnfactory) ApplicationContext.GetAutowiRecapableBeAnfactory (); pour (TenantConfigentity Entity: TenantConfigentities) {string beankey = dataSourceUtil.getDataSourceBeanKey (entity.gettenantKey ()); // Si la source de données a été enregistrée dans le printemps, ne réinscrivez pas si (applicationContext.ContainsBean (Beankey)) {DruidDataSource existantDataSource = applicationContext.getBean (beankey, druiddatasource.class); if (IssameDataSource (existantDataSource, entité)) {continuer; }} // assembler le bean abstractBeAnDefinition BeanDefinition = GetBeAnDefinition (Entity, Beankey); // Enregistrer Bean Beanfactory.RegisterBeAnDefinition (Beankey, BeanDefinition); // Mettez-le dans la carte, notez que l'objet bean a été créé tout à fait TargetDataSources.put (Beankey, ApplicationContext.getBean (Beankey)); } // Définissez l'objet de carte créé sur TargetDataSources; dynamicDataSource.setTargetDataSources (cibleDataSources); // Cette opération doit être effectuée avant que l'AbstractroutingDataSource ne soit réinitialisé les datasources résolus de cette manière, seule la commutation dynamique prendra effet DynamicDataSource.AfterProperTesTet (); } / ** * Assemblez la source de données Spring Bean * / Private AbstractBeAndefinition GetBeAndefinition (TenantConfigientity Entity, String Beankey) {BeanDefinitionBuilder Builder = BeanDefinitionBuilder.GenericBeanDefinition (DruidDatasource.Class); builder.getBeAnDefinition (). SetAttribute ("ID", Beankey); // Autres configurations Hériter de DefaultDataSource builder.setParentName (default_data_source_bean_key); builder.setinitMethodName ("init"); builder.setDestroyMethodName ("close"); builder.addpropertyValue ("nom", beankey); builder.addpropertyValue ("URL", dataSourceutil.getjdbcurl (entity.getdburl ())); builder.adddPropertyValue ("nom d'utilisateur", entity.getDbuser ()); builder.adddPropertyValue ("mot de passe", entity.getDBPassword ()); builder.adddPropertyValue ("ConnectionProperties", DataSourceUtil.getConnectionProperties (entity.getDBPublicKey ())); return builder.getBeAnDefinition (); } / ** * Déterminez si la surface de données dans le conteneur de printemps est conforme aux informations sur l'ourlet de données de la base de données * Remarque: Il n'y a pas de jugement ici sur public_key, car les trois autres informations peuvent essentiellement être déterminées comme étant uniques * / Boolean privé Issamedatasource (druiddatasource existance = Objets.equals (existantDataSource.getUrl (), DataSourceUtil.getjdbcurl (entity.getdburl ())); if (! SameUrl) {return false; } boolean SameUser = objets.equals (existantDataSource.getUserName (), entity.getDBuser ()); if (! SameUser) {return false; } essayez {String decryptPassword = configTools.decrypt (entity.getDBPublicKey (), entity.getDBPassword ()); return objets.equals (existantDataSource.getPassword (), decryptPassword); } catch (exception e) {log.Error ("Vérification du mot de passe de la source de données a échoué, exception: {}", e); retourne false; }}}printemps-data-source.xml
<! - Introduire le fichier de configuration JDBC -> <contexte: propriété-placeholder location = "classPath: data.properties" Ignore-unResolvable = "true" /> <! - public (par défaut) source de données -> <bean id = "defaultDatasource" init-méthod = "init" destrust-method = "close"> <! Value = "$ {ds.jdbcurl}" /> <propriété name = "username" value = "$ {ds.user}" /> <propriété name = "mot de passe" value = "$ {ds.password}" /> <! - Configurer la taille de l'initialisation, le minimum, et maximum -> <propriété name name = "maxactive" value = "10" /> <! - Configurez le temps pour attendre la connexion à la délai d'expiration en millisecondes -> <propriété name = "maxwait" value = "1000" /> <! - Configurez combien de temps il faut pour détecter la connexion inactive qui doit être fermée en millisecondes -> <propriété name = "Configure TimeBetweevictionRunsmillis" Pour détecter une fois, détectez la connexion inactive qui doit être fermée en millisecondes -> <propriété name = "TimeBetweenEvictionRunsmilis" Value = "5000" /> <! - Configurez le temps qu'il faut pour détecter une fois, détecter la connexion inactive qui doit être fermée en millisecondes -> <propriété Name = "TimeBetweinvictionRunsmillis" Pour une connexion pour survivre dans la piscine, en millisecondes -> <propriété name = "minevictableidletimemillis" value = "240000" /> <propriété name = "validationQuery" value = "select 1" /> <! - Unit: Seconds Temps pour détecter sécurité. Lorsque vous demandez une connexion, si le temps d'inactivité est supérieur à TimeBetweenEvictionRunsMillis, effectuez ValidationQuery pour détecter si la connexion est valide-> <propriété name = "Test WhemberIdle" Value = "True" /> <! - Exécuter ValidationQuery pour détecter si la connexion est valide lors de la demande de connexion. Cette configuration réduira les performances. -> <propriété name = "TestOnBorrow" value = "true" /> <! - Exécutez ValidationQuery lors du renvoi de la connexion pour vérifier si la connexion est valide. Faire cette configuration réduira les performances. --> <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> <!-- Transaction Manager--> <bean id="txManager"> <property name = "dataSource" ref = "MultipledataSource" /> </ank> <! - Multi-data Source -> <bean id = "MultipledataSource"> <propriété name = "defaultTargetDataSource" Ref = "DefaultDatasource" /> <propriété name = "TargetDatasources"> <aph </ map> </ propriété> </ank> <! - Annotation Transaction Manager -> <! - La valeur de commande ici doit être supérieure à la valeur de commande de DynamicDataSourceSpectAdvice -> <Tx: Annotation-Arive Transaction-manager = "txManager" Order = "2" /> <! - Créer SQLSessionFactory et Spécifiez la source de données -> <Eb = "MAINSQU <propriété name = "dataSource" ref = "multipledataSource" /> </ bean> <! - Le nom du package où se trouve l'interface DAO, le printemps trouvera automatiquement le dao sous elle -> <bean id = "MAINSQLMAPPER"> <propriété name = "SqlSessionFactoryBeAnname" Value = "MAINSQLESSESSEFACTORY" /> <près-nom = BasEpackage " Value = "ABC * .DAO" /> </ bean> <bean id = "DefaultSqlSessionFactory"> <propriété name = "DataSource" Ref = "DefaultDataSource" /> </ Bean> <bean id = "DefaultSqlMapper"> <propriété named = "SqlSessionFactoryBeAnname" Value = "DefaultsqlSessionFactory" Value = "ABCBASE.DAL.DAO" /> </BEAN> <! - Autre configuration omise ->DynamicdatasourceSpectAdvice
Changer automatiquement les sources de données en utilisant AOP pour référence uniquement;
@ Slf4j @ aspect @ composant @ commande (1) // Veuillez noter: l'ordre ici doit être inférieur à l'ordre de TX: axé sur l'annotation, c'est-à-dire d'exécuter d'abord la section dynamicdatasourceSpectAdvice, puis exécuter la section de transaction pour obtenir la source de données finales @enableSpectJautoproxy (proxytargetclass = true) Classe publique dynamique @Around ("EXECUTION (* ABC * .Controller. *. * (..))") Objet public Donound (ProcedingJoinpoint JP) lance Throws {servleTrequestAttributes sra = (servLetRequestAttributes) requestContextholder.getRequestAttributes (); HttpServLetRequest request = sra.getRequest (); HttpServletResponse Response = sra.getResponse (); String TenantKey = request.GetHeader ("locataire"); // Le front-end doit passer dans l'en-tête du locataire, sinon 400 seront retournés si (! StringUtils.hastext (TenantKey)) {webutils.tohttp (réponse) .SenDerror (httpservletResponse.sc_bad_request); retourner null; } log.info ("clé de locataire actuelle: {}", TenantKey); DataSourCeContexTholder.SetDataSourcekey (TenantKey); Résultat de l'objet = jp.proceed (); DataSourCeContexTholder.CleardAtaSourceKey (); Résultat de retour; }}Résumer
Ce qui précède est la méthode d'implémentation de l'enregistrement dynamique de printemps de plusieurs sources de données introduites par l'éditeur. J'espère que ce sera utile à tout le monde. Si vous avez des questions, veuillez me laisser un message et l'éditeur répondra à tout le monde à temps. Merci beaucoup pour votre soutien au site Web Wulin.com!