Recientemente, estamos trabajando en aplicaciones SaaS. La base de datos adopta una arquitectura de múltiples schema de una sola instancia (consulte la referencia 1 para más detalles). Cada inquilino tiene un esquema independiente, y toda la fuente de datos tiene un esquema compartido, por lo que es necesario resolver el problema de la adición dinámica y la eliminación y el cambio de fuentes de datos.
Después de buscar muchos artículos en línea, muchos de ellos hablan sobre la configuración de la fuente de datos del esclavo maestro, o han determinado la configuración de la fuente de datos antes de que se inicie la aplicación, y rara vez hablan sobre cómo cargar dinámicamente la fuente de datos sin cerrar, por lo que escribí este artículo como referencia.
Técnicas utilizadas
Ideas
Cuando entra una solicitud, determine el inquilino al que pertenece el usuario actual y cambie a la fuente de datos correspondiente en función de la información del inquilino, y luego realice las operaciones comerciales posteriores.
Implementación del código
InquilcConfigEntity (Información del inquilino) @equalsAndHashCode (llamadas = falso)@data@fielddefaults (nivel = accessLevel.private) clase pública Inquilcigentity { / ***ID de inquilino ** / Integer Tenantid; / ***Nombre del inquilino **/ String TenantName; / ***Clave de nombre del inquilino **/ String Tenantkey; / ***URL de la base de datos **/ String dburl; / ***Nombre de usuario de la base de datos **/ String dbuser; / ***Contraseña de base de datos **/ String dbpassword; / ***Base de datos public_key **/ String dbpublickey;} dataSourceUtil (clase de herramienta de ayuda, no esencial) clase pública dataSourceutil {private static final string data_source_bean_key_suffix = "_data_source"; cadena final estática privada JDBC_URL_ARGS = "? UseUnicode = true & caracterSencoding = UTF-8 & UseOldaliasmetAdatabeHavior = True & ZeroDatetimeBehavior = Converttonull"; statal final static private Connection_properties = "config.decrypt = true; config.decrypt.key ="; / ** * Clave de frijol spring para empalmar las fuentes de datos */ public static String getDataSourceBeankey (string inquiltey) {if (! StringUtils.hastext (inquiltey)) {return null; } return inquilpey + data_source_bean_key_suffix; } / ** * empalme completo jdbc url * / public static string getjdbcurl (string baseUrl) {if (! StringUtils.hastext (baseUrl)) {return null; } return BaseUrl + jdbc_url_args; } / *** Propiedades de conexión Druid completa empalmadas* / public static string getConnectionProperties (String publickey) {if (! StringUtils.hastext (public Key)) {return null; } return Connection_Properties + PublicKey; }}DataSourCeTexTholder
Use ThreadLocal para guardar el nombre de la clave de origen de datos del subproceso actual e implementar los métodos establecidos, obtenidos y borrosos;
Class pública DataSourCeCtexTholder {private Static final ThreadLocal <String> dataSourceKey = new HereTablethEltLocal <> (); public static void setDataSourceKey (string inquiltey) {dataSourceKey.set (inquilino); } public static string getDataSourceKey () {return dataSourceKey.get (); } public static void clearDataSourceKey () {dataSourceKey.remove (); }}DynamicDataSource (punto clave)
Heredar AbstrutingDataSource (se recomienda leer su código fuente para comprender el proceso de conmutación dinámica de fuentes de datos) y realizar una selección dinámica de fuentes de datos;
Public Class DynamicDataSource extiende AbstrutingDataSource {@aUtowired AplicationContext ApplicationContext; @Lazy @aUtowired Private DynamicDataSourcesummoner Summoner; @Lazy @aUtowired private inquilcigdao inquilconfigdao; @Override String protegido DetetInECRENTLOVELOPKEY () {String Tenantkey = DataSourCeTexTholder.getDataSourceKey (); return dataSourceutil.getDataSourceBeankey (inquilino); } @Override DataSource DetdetAnTargetDataSource () {String inquilino = dataSourCeCtexTholder.getDataSourceKey (); String Beankey = DataSourceUtil.getDataSourceBeankey (inquilino); if (! stringUtils.hastext (inquilantkey) || applicationContext.ContainsBean (BeanKey)) {return super.determinEtArgetDataSource (); } if (TenantConfigdao.exist (inquilino)) {Summoner.RegisterDynamicDataSources (); } return super.determinetArgetDataSource (); }}DynamicDataSourcesMoner (punto clave de enfoque)
Cargue la información de la fuente de datos de la base de datos y ensamble y registre dinámicamente los frijoles de resorte.
@Slf4j@componentPublic class DynamicDataSourcesUmoner implementa ApplicationListener <Contextrefreshedevent> {// consistente con la ID de origen de datos predeterminada de Spring-Data-Source.xml Private Static Final String Default_Data_Source_Bean_Key = "DefaultDataSource"; @AUtowired private configuriveApplicationContext ApplicationContext; @Autowired Private DynamicDataSource DynamicDataSource; @AUTOWIREDIREDIRIRD PRIVADO HISITCIGDAO HISHCONFIGDAO; booleano estático privado cargado = falso; / *** Ejecutar después de que se complete la carga de resorte*/ @Override public void onApplicationEvent (contextrefreshedevent event) {// Evite la ejecución repetida if (! Loaded) {Loaded = true; intente {RegistroDynamicDataSources (); } capt (excepción e) {log.error ("Fallación de inicialización de la fuente de datos, excepción:", e); }}}} / *** Lea la configuración de DB del inquilino desde la base de datos e inyecte dinámicamente el contenedor Spring* / public void registroDynamicDataSources () {// Obtener la configuración de DB para todos los inquilinos List <Nienfigentity> TenantConfigentities = TenantConfigdao.listall ();; if (collectionUtils.isEmpty (inquilinconfigentities)) {lanzar nueva ilegalStateException ("Falló la inicialización de la aplicación, configure primero la fuente de datos"); } // Registre el bean de origen de datos en el contenedor addDataSourceBeans (inquilinoconfigentidades); } / *** Crear frijoles basados en DataSource y regístrese en el contenedor* / private void addateSourceBeans (List <NENTCONFIGENTITY> TenantConfigEntities) {Map <Object, Object> TargetDataSources = Maps.newlinkedHashMap (); DefaultListableBeanFactory BeanFactory = (DefaultListableBeanFactory) ApplicationContext.getAutowIRECapableBeanFactory (); para (entidad inquilinccigentity: inquiltcigeNities) {string beankey = dataSourceUtil.getDataSourceBeankey (entity.getteNantkey ()); // Si la fuente de datos se ha registrado en Spring, no vuelva a registrarse si (ApplicationContext.ContainsBean (BeanKey)) {DruidDataSource existeDataSource = ApplicationContext.getBean (BeanKey, DruidDataSource.class); if (issamedataSource (existsDataSource, entidad)) {continuar; }} // ensamble Bean AbstractBeanDefinition Beandefinition = GetBeanDefinition (Entity, BeanKey); // registrar beanfactory.RegisterBeanDefinition (BeanKey, Beandefinition); // Ponerlo en el mapa, tenga en cuenta que el objeto Bean se creó justo ahora TargetDataSources.put (BeanKey, ApplicationContext.GetBean (BeanKey)); } // Establecer el objeto de mapa creado en TargetDataSources; dynamicDataSource.settArgetDataSources (TargetDataSources); // Esta operación debe realizarse antes de que el AbstrutingDataSource sea reinicializado resuelto DataSources de esta manera, solo la conmutación dinámica tendrá efecto DynamicDataSource.AfterPropertIesset (); } / ** * Ensamble la fuente de datos Spring Bean * / Private AbstractBeanDefinition GetBeanDefinition (InquilcConfigEntity Entity, String BeanKey) {BeandefinitionBuilder Builder = beandefinitionBuilder.GenericBeanDefinition (druidDataSource.classs); Builder.getBeanDefinition (). SetAttribute ("Id", BeanKey); // Otras configuraciones heredan defaultDataSource Builder.setParentName (default_data_source_bean_key); Builder.SetInitMethodName ("init"); Builder.setDestroyMethodName ("Cerrar"); Builder.AddPropertyValue ("Nombre", BeanKey); Builder.AddPropertyValue ("URL", DataSourceUtil.getJdbCurl (entity.getDburl ())); builder.addpropertyValue ("nombre de usuario", entity.getdbuser ()); builder.AddPropertyValue ("Password", entity.getdbpassword ()); Builder.AddPropertyValue ("ConnectionProperties", DataSourceUtil.getConnectionProperties (entity.getDBPublicKey ())); return builder.getBeanDefinition (); } / *** Determine si la fuente de datos en el contenedor Spring es consistente con la información de la fuente de datos de la base de datos* Nota: No hay un juicio aquí en public_key, porque las otras tres información se pueden determinar básicamente como un único* / privado booleano issamedataSource (DruiddataSource existesource, inquilino de inquilino) {booleaneuMeurce mismo. Objects.Equals (existSdataSource.getUrl (), dataSourceUtil.getJdbCurl (entity.getDburl ())); if (! mismo mismo) {return false; } boolean sameuser = objects.equals (existsDataSource.getUsername (), entity.getdbuser ()); if (! mismouser) {return false; } try {String DecryptPassword = configTools.decrypt (entity.getdbpublickey (), entity.getdbpassword ()); return Objects.equals (existsDataSource.getPassword (), DecryptPassword); } catch (Exception e) {log.error ("Fallado de verificación de contraseña de fuente de datos, excepción: {}", e); devolver falso; }}}spring-data-source.xml
< value = "$ {ds.jdbcurl}" /> <propiedad name = "username" value = "$ {ds.user}" /> <propiedad name = "contraseña" value = "$ {ds.password}" /> <!-Configurar el tamaño de la inicialización, el mínimo y el máximo-> <name de propiedad = "InitialSize" Value = "5" /> <<sperty Name = "Minidle" Value "Value" 2 " /" 2 ". name = "maxactive" value = "10" /> <!-Configure el tiempo para esperar la conexión al tiempo de espera en MilliseConds-> <Property name = "maxwait" value = "1000" /> <!-Configure cuánto tiempo tarda en detectar la conexión inactiva que debe cerrarse en MillisEconds-> <nombre de propiedad = "TimeBetBetEnbil Detectar una vez, detectar la conexión inactiva que debe cerrarse en milisegundos-> <propiedad name = "TimeBetweenEvictionRunsMillis" value = "5000" /> <!-Configure cuánto tiempo tarda en detectar una vez, detectar la conexión inactiva que debe cerrarse en MilliseConds-> <nombre de propiedad = "TimebetweeVictionRunrillis" Value = "5000" /<<!--CONEXIMIENTO TIEMPO A LA CONECTURA ANECTURA A LA CONECTURA ANECTUNE A LA CONEXIMIENTO A LA CONEXIMINE A LA CONEXIMIENTO DE LA CONEXIMINE A LA CONEXIMIENTO "! Sobrevivir en el grupo, en MilliseConds-> <Property name = "mineVictableIdletImEmillis" value = "240000" /> <Property name = "valueQuery" valor = "seleccionar 1" /> <!-Unit: segundos, tiempo de tiempo de tiempo para detectar si la conexión es válida-> <name de propiedad = "ValidationQuery TimeOut" Valor = "60" /> <!-Configurar a la verdadera, verdadera, verdadera, verdadera, no afectan el rendimiento. Al solicitar una conexión, si el tiempo de inactividad es mayor que el timebetweenEvictionRunsMillis, realice ValidationQuery para detectar si la conexión es válida-> <propiedad de propiedad = "testIlleidle" value = "true" /> <!-Ejecute ValidationQuery para detectar si la conexión es válida cuando se solicita una conexión. Esta configuración reducirá el rendimiento. -> <propiedad name = "testOnBorrow" value = "true" /> <!-Ejecute ValidationQuery al devolver la conexión para verificar si la conexión es válida. Hacer esta configuración reducirá el rendimiento. -> <Property name = "testOnreturn" value = "false" /> <!-config filtre-> <propiedad name = "filters" value = "config" /> <Property name = "ConnectionProperties" value = "config.decrypt = true; config.decrypt.key = $ {ds.publickey}" /> < /bean> <! name = "DataSource" ref = "multipledataSource"/> </bean> <!-Multi-data Source-> <bean id = "multi-mipleledataSource"> <propiedad name = "defaultTargetDataSource" ref = "DefaultDataSource"/> <Property Name = "TargetDataSources"> <MAP> <Entrada = "Key =" DefaulthataSource "Value-Refef =" "Property Name =" TargetAdataSources "> <MAP> <Entrada =" Key = "Key =" Defaultsource "Value-Refef =" </map> </property> </bean> <!-Annotation Transaction Manager-> <!-El valor de pedido aquí debe ser mayor que el valor de pedido de DynamicDataSourCeAspectAdVice-> <tx: Annotation-Drived Transaction-Manager = "TXManager" Order = "2"/> <! Crea sqlSessionFactory y especifica la fuente de datos-> <<r Bean Id = "shory" name = "DataSource" ref = "multipledataSource"/> </bean> <!-El nombre del paquete donde se encuentra la interfaz DAO, Spring encontrará automáticamente el DAO en IT-> <Bean ID = "MainsqlMapper"> <Property Name = "SqlSessionFactoryBeanNeNName" Value = "MainsqlSessionFactory"/> <Property Name = "BasePackage" BasePackage "" BasePackage " value = "ABC*.DAO"/> </Bean> <Bean id = "DefaultSQLSessionFactory"> <Property Name = "DataSource" ref = "DefaultDataSource"/> </bean> <bean id = "defaultsqlMapper value = "abcbase.dal.dao"/> </bean> <!-Otra configuración omitida->DynamicDataSourceaspectAdvice
Cambiar automáticamente las fuentes de datos utilizando AOP solo para referencia;
@Slf4j@aspecto@componente@orden (1) // Tenga en cuenta: el pedido aquí debe ser menor que el orden de tx: anotación impulsada, es decir, ejecute primero la sección DynamicDataSourCeaseSpectAdVice, y luego ejecute la sección de transacciones para obtener la fuente de datos final @EnableAspectJaUtoProxy (proxyTargetClass = true) @Around ("Ejecution (*ABC*.Controller.*.*(..)") Objeto público doaround (procedimientojoinpoint jp) lanza lando {servletRequestatTributes sra = (ServLetRequestattributes) requestContextholder.getRequestTributes (); HttpservletRequest request = sra.getRequest (); HttpservletResponse respuesta = sra.getResponse (); Cadena inquilino = request.getheader ("inquilino"); // El front-end debe pasar al encabezado del inquilino, de lo contrario, 400 se devolverán si (! StringUtils.hastext (inquilino)) {webutil.tohttp (respuesta) .senderRor (httpservletResponse.sc_bad_request); regresar nulo; } log.info ("clave del inquilino actual: {}", inquilino); DataSourCeTexTholder.SetDataSourceKey (inquilino); Resultado del objeto = jp.proceed (); DataSourCeTexTholder.CLearDataSourceKey (); resultado de retorno; }}Resumir
Lo anterior es el método de implementación del registro dinámico de Spring de múltiples fuentes de datos introducidas por el editor. Espero que sea útil para todos. Si tiene alguna pregunta, déjame un mensaje y el editor responderá a todos a tiempo. ¡Muchas gracias por su apoyo al sitio web de Wulin.com!