Recentemente, estamos trabalhando em aplicativos SaaS. O banco de dados adota uma única arquitetura multi-esquema de instância (consulte a referência 1 para obter detalhes). Cada inquilino possui um esquema independente e toda a fonte de dados possui um esquema compartilhado, por isso é necessário resolver o problema de adição dinâmica e exclusão e comutação de fontes de dados.
Depois de pesquisar muitos artigos on-line, muitos deles falam sobre a configuração da fonte de dados mestre-escravos ou determinaram a configuração da fonte de dados antes do início do aplicativo e raramente falo sobre como carregar dinamicamente a fonte de dados sem desligar, então escrevi este artigo para referência.
Técnicas usadas
Idéias
Quando chegar uma solicitação, determine o inquilino ao qual o usuário atual pertence e mude para a fonte de dados correspondente com base nas informações do inquilino e execute operações comerciais subsequentes.
Implementação de código
TenantConfigentity (Informações do inquilino) @equalsandHashCode (Callsuper = false)@data@fieldDefaults (nível = accessLevel.private) classe pública tenantConfigentity { / ***inquilant id ** / integer tenantID; / ***Nome do inquilino **/ string inntentName; / ***Chave do nome do inquilino **/ String inquilantkey; / ***URL do banco de dados **/ string dburl; / ***nome de usuário do banco de dados **/ string dbuser; / ***Senha do banco de dados **/ string dbpassword; /** String final estática privada jdbc_url_args = "? useunicode = true & caracteryncoding = utf-8 & useoldaliasMetadatAbEHavior = true & zerodateTimeBeHavior = converttonull"; String final estática privada Connection_properties = "config.Decrypt = true; config.Decrypt.key ="; / ** * Chave do feijão da primavera para fontes de dados de emenda */ public static string getDataSourceBeanKey (String thentKey) {if (! StringUtils.hastext (tenantkey)) {return null; } retornar tenantkey + data_source_bean_key_suffix; } / ** * Splicing completo jdbc url * / public static string getjdbcurl (string baseurl) {if (! StringUtils.hastext (baseurl)) {return null; } retornar baseurl + jdbc_url_args; } / *** Propriedades de conexão com druid completas emendas* / public static string getConnectionProperties (String publicKey) {if (! StringUtils.hastext (publicKey)) {return null; } retornar conexão_properties + publicKey; }}DataSourceContextholder
Use ThreadLocal para salvar o nome de chave da fonte de dados do thread atual e implementar o conjunto, obter e limpar métodos;
classe pública DataSourceConteXTHOLDER {private Static Final ThreadLocal <String> DataSourceKey = new IritableThreadLocal <> (); public static void SetDataSourceKey (String thentKey) {DataSourceKey.Set (TenantKey); } public static string getDataSourceKey () {return dataSourceKey.get (); } public static void clearDataSourceKey () {DataSourceKey.remove (); }}DynamicDataSource (ponto -chave)
Herite o AbstractRoutingDataSource (é recomendável ler seu código -fonte para entender o processo de alternar dinamicamente as fontes de dados) e realizar a seleção dinâmica de fontes de dados;
classe pública DynamicDataSource estende o abstractroutingDataSource {@AUTOWIRED APPLICATIONCONTEXT APLACKCONTEXT; @Lazy @Autowired Private DynamicDataSourcesummoner Invocador; @Lazy @Autowired Private TenantConfigdao TenantConfigdao; @Override Protected String determinanteCurrentLookupKey () {String thentKey = DataSourceContextholder.getDataSourceKey (); Retornar DataSourceutil.getDataSourceBeanKey (inquilino); } @Override DataSource Protected DeconmarETargetDataSource () {String thentKey = DataSourceContextholder.getDataSourceKey (); String beankey = DataSourceutil.getDataSourceBeanKey (inquilino); if (! stringUtils.hastext (tenantKey) || ApplicationContext.ContainsBean (beankey)) {return Super.DetermineTargetDataSource (); } if (tenantConfigdao.exist (tenantKey)) {summoner.registerdynamicDatasources (); } retornar super.DetermineTargetDataSource (); }}DynamicDataSourcesummoner (ponto -chave do foco)
Carregue as informações da fonte de dados do banco de dados e montam e registre dinamicamente os grãos de mola.
@Slf4j@componentPublic Classe dinâmica DynamicDatasourcesummoner implementa ApplicationListener <contextrefreshedEvent> {// consistente com o ID da fonte de dados padrão de spring-data-source.xml private static string final default_data_source_bean_key = "defaultDataSource"; @AUTOWIRED PRIVADO CONFIGURABLEAPPLICATIONCONTEXT ApplicationContext; @AUTOWIRED PRIVADO DINGRASSOUSOURCE DynamicDataSource; @AUTOWIRED PRIVADO TENANTCONFIGDAO TENANTCONFIGDAO; Private estático booleano carregado = false; / *** Execute após o carregamento da mola é concluído*/ @Override public void onApplicationEvent (contextrefreshedEvent evento) {// impedir a execução repetida se (! Carregada) {carregada = true; tente {registrdynamicDatasources (); } catch (Exceção e) {log.error ("Inicialização da fonte de dados falhou, exceção:", e); }}}} / *** Leia a configuração do banco de dados do inquilino do banco de dados e injete dinamicamente o contêiner de mola* / public void registrDentynicDataSources () {// Obtenha a configuração do banco de dados para todos os inquilinos (tenantConfigentity> tenantConfigentities = tenantconfigdao.listll; if (collectionUtils.IsEmpty (TenAntConfigentities)) {lança nova ilegalStateException ("A inicialização do aplicativo falhou, configure a fonte de dados primeiro"); } // Registre o feijão da fonte de dados no contêiner addDataSourceBeans (tenantConfigentities); } / *** Crie feijão com base no DataSource e registre -se no contêiner* / private void addDataSourceBeans (list <tenantConfigentity> tenantConfigentities) {map <object, object> TargetDatasources = maps.newlinkedHashmap (); DefaultListableBeanFactory BeanFactory = (DefaultListableBeanFactory) ApplicationContext.GETAUTOWIRECAPABLEBeanFactory (); para (entidade tenantConfigentity: tenantConfigentities) {string beankey = DataSourceutil.getDataSourceBeanKey (entity.gettenantKey ()); // Se a fonte de dados tiver sido registrada na primavera, não se registre novamente se (ApplicationContext.ContainsBean (beankey)) {druiddataSource existeDataSource = ApplicationContext.getBean (Beankey, druiddatasource.class); if (issamedataSource (existdataSource, entidade)) {continua; }} // Assemble Bean AbstractBeandEfinition beandEfinition = getBeandEfinition (entidade, beankey); // registra bean beanfactory.registerbeandEfinition (Beankey, BeandEfinition); // Coloque no mapa, observe que o objeto Bean foi criado agora agora TargetDatasources.put (Beankey, ApplicationContext.getBean (Beankey)); } // Defina o objeto de mapa criado como TargetDataSources; DynamicDataSource.SetTargetDataSources (TargetDataSources); // Esta operação deve ser realizada antes que o AbstractRoutingDataSource seja reinicializado ResolvEDDataSources dessa maneira, apenas a comutação dinâmica terá efeito dinâmicoDataSource.AfterPropertiEsset (); } / ** * Assenhe o feijão da fonte de dados * / Private AbstractBeandEfinition getBeandEfinition (entidade tenantConfigentity, string beankey) {beandefinitionbuilder construtor = beandefinitionbuilder.GenericBeandEfinition (druiddataSource.cllass); builder.getbeanndefinition (). setAttribute ("id", beankey); // Outras configurações herdam DefaultDataSource Builder.SetParentName (default_data_source_bean_key); builder.setInitMethodName ("init"); builder.setDestroyMethodName ("Close"); construtor.addPropertyValue ("Nome", Beankey); builder.addPropertyValue ("url", dataAsourceutil.getjdbcurl (entity.getdburl ())); builder.addpropertyValue ("nome de usuário", entity.getdbuser ()); builder.addPropertyValue ("senha", entity.getDbPassword ()); builder.addPropertyValue ("ConnectionProperties", DataSourceutil.getConnectionProperties (entity.getDbpublicKey ())); retornar construtor.getBeandEfinition (); } / *** Determine se a fonte de dados no recipiente de mola é consistente com as informações da fonte de dados do banco de dados* Nota: Não há julgamento aqui no Public_key, porque as outras três informações podem basicamente ser determinadas como exclusivas* / private booleansEdataSource (druiddatasource existdataSource, inquérito e ingresso (ingresso) (druiddataSource) / calolean e ingresso) Objects.Equals (existdataSource.getURL (), DataSourceUtil.getjdbcurl (entity.getdburl ())); if (! Sameurl) {return false; } boolean SameUser = Objects.Equals (existdataSource.getUserName (), entity.getdbuser ()); if (! SameUser) {return false; } tente {string decryptpassword = configtools.decrypt (entity.getdbpublicKey (), entity.getdbpassword ()); Return Objects.Equals (existdataSource.getpassword (), descriptoppassword); } catch (Exceção e) {log.error ("Verificação da senha da fonte de dados falhou, exceção: {}", e); retornar falso; }}}Spring-Data-source.xml
<!-Introducer o arquivo de configuração do JDBC-> <Contexto: Property-placeholder Location = "ClassPath: Data.Properties" Ignore-unresolvable = "true"/> <!-Public (Padrão) Fonte de Data-> <Bean Id = "DefaultDataSource" InitD = "init" "Meth =" Close ">" Close = "> <! value = "$ {ds.jdbcurl}" /> <propriedade name = "userName" value = "$ {ds.user}" /> <propriedade name = "senha" value = "$ {ds.password}" /> <!-Configure Tamanho da inicialização, mínimo e máximo-> <nome da propriedade "" Initialsize "" <! name = "maxactive" value = "10" /> <!-Configure o tempo para aguardar a conexão com o tempo limite em milissegundos-> <propriedade name = "maxwait" value = "1000" /> <!-configure quanto tempo é necessário para detectar a conexão ociosa que precisa ser fechada em MilleSoLSONSILS-> <propriedades "" timernween " É preciso detectar uma vez, detecte a conexão inativa que precisa ser fechada em milissegundos-> <propriedade name = "timebetweenEvictionRunsmillis" value = "5000" /> <!-Configure quanto tempo leva para detectar uma vez, detectar a conexão ociosa que precisa ser fechada em MillisElSoLSILSONSONSELS-> <nome da timenTenween, "TimeBetwen," TimeVenTtytr, que precisa ser fechada! Tempo mínimo para uma conexão sobreviver no pool, em milissegundos-> <propriedade name = "minevictableIdLetimEmillis" value = "240000" /> <propriedades name = "validationQuery" value = "selecione 1" /> <!-unidade: segundos, tempo de tempo para detectar se a conexão "é válida-> <) /> <! e garante segurança. Ao solicitar uma conexão, se o tempo de marcha lenta for maior que a TimeweenEvictionRunSmillis, execute a ValidationQuery para detectar se a conexão é válida-> <propriedade name = "testhileidle" value = "true" /> <!-Execute a ValidationQuery para detectar se a conexão é válida quando se aplica a uma conexão. Esta configuração reduzirá o desempenho. -> <Property name = "testOnBorrow" value = "true" /> <!-Execute validationQuery ao retornar a conexão para verificar se a conexão é válida. Fazer essa configuração reduzirá o desempenho. -> <Propriedade name = "testOnReturn" value = "false" /> <!-config filtro-> <propriedade name = "filters" value = "config" /> <propriedade name = "ConnectionProperties" value = "config.decrypt = true; config.decrypt.key = $ {ds.publickey}" /> name = "DataSource" ref = "multiplledataSource"/> </i bean> <!-fonte multi-dados-> <bean id = "multiplledatasource"> <names name = "defaultTargetDataSource" rec = "defaultDataSource"/> <nome da propriedade "tiraDataDuCE"> <paps> value-ref = "defaultDataSource"/> </map> </property> </bean> <!-Gerenciador de transações de anotação-> <!-O valor da ordem aqui deve ser maior que o valor da ordem do dinâmicoDataSourCeasPesionVice-> <T. <bean id = "mainsqlSessionFactory"> <propriedade name = "DataSource" ref = "multiplledatasource"/> </i bean> <!-o nome do pacote em que a interface Dao está localizada, o Spring encontrará automaticamente o Dao em TI-> <Bean Id = "MainsqnMapper"> <propriedades "" SQLSFLAFTBE Nome = "BasEpackage" value = "abc*.dao"/> </ Bean> <bean id = "defaultSqlSessionFactory"> <propriedade name = "DataSource" ref = "defaultDataSource"/> </bean> <bean id = "defaultSqnmapper"> <faften> <Nome da propriedade = "BasEpackage" value = "abcbase.dal.dao"/> </i bean> <!-Outras configurações omitidas->DynamicDataSourCeaspectadvice
Alterne automaticamente as fontes de dados usando a AOP apenas para referência;
@Slf4j@aspecto@componente@order (1) // por favor, observe: o pedido aqui deve ser menor que a ordem de tx: acionada por anotação, ou seja, execute primeiro a seção DynamicDataSourCeasPeSpectVice e depois execute a seção de transação para obter a fonte de dados final @EnableasPeTJAUTOPROXY (Proxy @Around ("Execução (*abc*.Controller.*.*(..))") Public Object Doaround (ProceedingJoinPoint JP) lança arremesso {servletRequestAttributes sra = (servletRequestatrributes) requestContextholder.GerequesterTtrributes (); HttpServletRequest request = sra.getRequest (); HttpServletResponse resposta = sra.getResponse (); String thentKey = request.getheader ("inquilino"); // O Front-End deve passar para o cabeçalho do inquilino; caso contrário, 400 será devolvido se (! retornar nulo; } log.info ("Chave do inquilino atual: {}", tenantKey); DataSourceContexTholder.SetDataSourceKey (inquilino); Resultado do objeto = jp.proeced (); DataSourceContextholder.clearDataSourceKey (); resultado de retorno; }}Resumir
O acima é o método de implementação do registro dinâmico da mola de várias fontes de dados introduzidas pelo editor. Espero que seja útil para todos. Se você tiver alguma dúvida, deixe -me uma mensagem e o editor responderá a todos a tempo. Muito obrigado pelo seu apoio ao site wulin.com!