Недавно мы работаем над SaaS -приложениями. В базе данных используется один экземпляр мульти-схема архитектуры (для получения подробной информации см. Ссылку 1). У каждого арендатора есть независимая схема, и весь источник данных имеет общую схему, поэтому необходимо решить проблему динамического сложения и удаления и переключения источников данных.
После поиска многих статей в Интернете многие из них рассказывают о конфигурации источника данных мастер-рабов, или они определили конфигурацию источника данных до начала приложения, и редко рассказывают о том, как динамически загрузить источник данных без выключения, поэтому я написал эту статью для справки.
Методы используются
Идеи
Когда вступает запрос, определите арендатора, к которому принадлежит текущий пользователь, и переключитесь на соответствующий источник данных на основе информации арендатора, а затем выполните последующие бизнес -операции.
Реализация кода
TenantConfigentity (Информация об аренде) @EqualsAndhashCode (callsuper = false)@data@fielddefaults (level = accesslevel.private) public class tenantConfigentity { / ***Идентификатор арендатора ** / integer tenantId; / ***Имя арендатора **/ String tenantName; / ***Имя арендатора клавиша **/ String enantkeke; / ***URL базы данных **/ String dburl; / ***Имя пользователя базы данных **/ String dbuser; / ***Пароль базы данных **/ String dbpassword; / ***База данных public_key **/ string dbpublickey;} dataSourceUtil (класс инструментов AIDANT) Приватная статическая конечная строка jdbc_url_args = "? useUnicode = true & hareverencoding = utf-8 & useralalaLiasMateAdatabehavior = true & ZeroDateTimeBehavior = convertonull"; Private Static Final String Connection_properties = "config.decrypt = true; config.decrypt.key ="; / ** * КЛЮЧЕСКИЙ КЛЮЧЕСКИЙ БОН ДЛЯ ИСТОЧНИКИ ДАННЫХ */ Public Static String GetDataSourceBeanKey (String TenantKey) {if (! StringUtils.hastext (venantkey)) {return null; } return tenantKey + data_source_bean_key_suffix; } / ** * Сплайсинг } return baseurl + jdbc_url_args; } / *** Сплайсированные свойства подключения друида* / public Static String getConnectionProperties (String publicKey) {if (! StringUtils.hastext (publickey)) {return null; } return connection_properties + publickey; }}DataSourceContextholder
Используйте Threadlocal для сохранения имени клавиши источника данных текущего потока и реализации методов SET, GET и CLEAR;
открытый класс dataSourceContexTholder {private Static Final Threadlocal <String> dataSourceKey = new InheritableThreadLocal <> (); public static void setDataSourcekey (String inantKey) {dataSourceKey.set (enantKey); } public Static String getDataSourceKey () {return dataSourcekey.get (); } public static void clearDataSourcekey () {dataSourceKey.remove (); }}DynamicDataSource (ключевой момент)
Унаследовать AbstractroutingDatasource (рекомендуется прочитать его исходный код, чтобы понять процесс динамического переключения источников данных) и реализовать динамический выбор источников данных;
Общедоступный класс DynamicDatasource Extends AbstractroutingDataSource {@Autowired Private ApplicationContext ApplicationContext; @Lazy @autowired private dynamicdatasourcesummoner summoner; @Lazy @autowired private tenantconfigdao tenantconfigdao; @Override Protected String DetrineCurrentLookupkey () {String tenantKey = dataSourceContexTholder.getDataSourceKey (); return dataSourceutil.getDatasourcebeankey (TenantKey); } @Override защищенный DataSource DetrineTargetDatasource () {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 (); }}DynamicDataSourcesMomoner (ключевая точка фокуса)
Загрузите информацию источника данных из базы данных и динамически собирайте и зарегистрируйте пружинные бобы.
@Slf4j@componentpublic class dynamicdatasourcesummoner реализует ApplicationListener <contextrefreshedevent> {// Соответствующий идентификатор источника данных по умолчанию Spring-data-source.xml Private Static Final String default_data_source_bean_key = "defauldatasource"; @Autowired private configururableapplicationcontext ApplicationContext; @Autowired private dynamicdatasource dynamicdatasource; @Autowired private tenantconfigdao tenantconfigdao; Частный статический логический загрузка = false; / *** Выполнить после завершения пружины. try {RegisterDynamicDatasources (); } catch (Exception e) {log.error ("Инициализация источника данных не удалась, исключение:", e); }}}} / *** Читать конфигурацию DB -арендатора из базы данных и динамически вводить контейнер Spring* / public void RegistermyNamicDataSources () {// Получить конфигурацию DB для всех арендаторов <tenantConfigentity> tenantConfigentity = TenantConfigDao.ListAll ();); if (collectiontils.isempty (tenantconfigentities)) {бросить новое allodalstateexception («Инициализация приложения не удалась, пожалуйста, настройте источник данных первым»); } // Зарегистрировать бон источника данных в контейнере addDataSourcebeans (TenantConfigenity); } / *** Создание бобов на основе данных и регистрации в контейнере* / private void addDataSourcebeans (список <tenantConfigentity> tenantConfigentities) {map <Object, Object> TargetDataSources = maps.newlinkedHashmap (); Default -stistablebeanfactory beanfactory = (default -lectablebeanfactory) ApplicationContext.getAutowireCapableBeanFactory (); для (enantconfigentity ortity: envantconfigentinests) {string beankey = dataSourceutil.getDatasourcebeankey (entity.gettenantkey ()); // Если источник данных был зарегистрирован весной, не перерегистрируйте if (ApplicationContext.containsbean (beankey)) {druiddataSource experaintasource = ApplicationContext.getBean (beankey, druiddatasource.class); if (issamedatasource (ExitsDataSource, Entity)) {Продолжить; }} // Сборник бобов AbstractBeanDefinition beandefinition = getBeanDefinition (Entity, Beankey); // зарегистрировать Bean Beanfactory.registerbeandefinition (Beankey, Beandefinition); // Поместите его в карту, обратите внимание, что объект Bean был создан только сейчас TargetDataSources.put (beankey, ApplicationContext.getBean (Beankey)); } // Установить созданный объект карты в TargetDataSources; DynamicDataSource.SetTargetDataSources (TargetDataSources); // Эта операция должна быть выполнена до того, как AbstractroutingDataSource будет повторно повторно ренициализирована ResolvedDataSources, только динамическое переключение вступит в силу DynamicDataSource.afterPropertiesset (); } / ** * Сборка источника данных Spring Bean * / Private AbstractBeandefinition getBeanDefinition (Entity TenantConfigentity, 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 («Имя», Beankey); builder.addpropertyvalue ("url", dataSourceutil.getJdbcurl (entity.getDburl ())); builder.addpropertyvalue («Имя пользователя», Entity.getDbuser ()); builder.addpropertyvalue ("пароль", entity.getdbpassword ()); builder.addpropertyValue ("ConnectionProperties", dataSourceutil.getConnectionProperties (entity.getDbpublickey ())); return Builder.getBeanDefinition (); } / *** Определите, соответствует ли данных в контейнере для весеннего контейнера с информацией о данных базы данных* Примечание. Здесь нет суждения на public_key, потому что три других информации в основном можно определить как уникальную* / частную логическую Issamedatasource (Boolean -SightDataSource, TenantConfigentity Entity) {boolan neaintasource, tenantconfigentity) Objects.equals (existsDatasource.getUrl (), dataSourceutil.getJdbcurl (entity.getDburl ())); if (! То же самое) {вернуть false; } boolean shideuser = objects.equals (existsdatasource.getusername (), entity.getdbuser ()); if (! simeUser) {вернуть false; } try {string decryptPassword = configtools.decrypt (entity.getDbpublickey (), entity.getDbpassword ()); возвращать объекты. } catch (Exception e) {log.Error ("Проверка пароля источника данных, исключение: {}", e); вернуть ложь; }}}Spring-Data-Source.xml
<!-Представьте файл конфигурации JDBC-> <Контекст: Property Placeholder location = "classPath: Data.Properties" Игнорировать Unresolvable = "true"/> <!-Public (Default) Источник данных-> <Bean Id = "Defauldatasource" init-method = "init" drester-method = "close"> <!-Basic ProteSerties, user, pasship = vorpert,-in erest-method = "close"> <!-Basic proteerties, user,-in ereste-method = "> <! value="${ds.jdbcUrl}" /> <property name="username" value="${ds.user}" /> <property name="password" value="${ds.password}" /> <!-- Configure initialization size, minimum, and maximum --> <property name="initialSize" value="5" /> <property name="minIdle" value="2" /> <property name = "maxactive" value = "10" /> <!-Настройте время, чтобы дождаться подключения к тайм-ауту в миллисекундах-> <name = "maxwait" value = "1000" /> <!-Настройка того, сколько времени требуется, чтобы обнаружить постоянное соединение, которое необходимо закрыть в миллисекундах-> <Property name. Определить один раз, обнаружите непрерывное соединение, которое необходимо закрыть в миллисекундах-> <name = "timebineEvictionRunsmillis" value = "5000" /> <!-Настройка того, сколько времени требуется, чтобы обнаружить один раз, обнаружение непрерывного соединения, которое необходимо закрыть в миллисекундах-> <свойство = «Министерство времени». Чтобы выжить в пуле, в Milliseconds-> <name = "minevictableIdleMemilis" value = "240000" /> <name = "valyationQuery" value = "select 1" /> <!-Блок: секунды, время ожидания для выявления того, является ли соединение действительным-> <свойство = "ValidationTimeUt" 60 " /> <!-evertive, что не может быть и верно, что не может быть истинно, что не может. При подаче заявки на подключение, если время простоя больше, чем TimeBowneEvictionRunsmillis, выполните ValidationQuery, чтобы определить, является ли соединение действительным-> <name = "testwhileidle" value = "true" /> <!-выполнить ValidationQuery, чтобы определить, является ли соединение действительным при подаче заявления на соединение. Эта конфигурация снизит производительность. -> <name = name = "testonBorrow" value = "true" /> <!-выполнить ValidationQuery при возврате соединения, чтобы проверить, является ли соединение действительным. Выполнение этой конфигурации снизит производительность. -> <name = name = "testonReturn" value = "false" /> <!-config filter-> <name = "filters" value = "config" /> <name = "connectionProperties" value = "config.decrypt = true; config.decrypt.key = $ {ds.publickey}" /> < /bean> <! name = "dataSource" ref = "multipledatasource"/> </bean> <!-Multi-Data Source-> <Bean Id = "MultipleDatasource"> <Property name = "defaultTargetDatasource" ref = "defaultDatasource"/> <properation = "targetDatasourcess"> <Map> <inptrastDatas "/> <свойство =" value-ref = "defaultDataSource"/> </map> </property> </bean> <!-Manager транзакции аннотации-> <!-Значение заказа здесь должно быть больше, чем значение порядка DynamicDataSourceaSpectepectadvice-> <TX: Annotation Transaction-Manager = "TXMANAGE <bean id = "mainsqlSessionFactory"> <property name = "dataSource" ref = "multileDataSource"/> </bean> <!-Имя пакета, где находится интерфейс DAO, Spring автоматически найдет DAO под ним name = "basepackage" value = "abc*.dao"/> </bean> <bean id = "defaultsqlSessionFactory"> <name = "dataSource" ref = "defaultDatasource"/> </bean> <bean id = "defaultsqlmapper"> name = "sqlSessionFactoryBaintoryBeanname" value = "defaultsqlSessionFactory"/> <name = "basepackage" value = "abcbase.dal.dao"/> </bean> <!-Другая конфигурация опущена->DynamicDataSourceSpectAdvice
Автоматически переключать источники данных с помощью AOP только для справки;
@Slf4j@asmover@component@order (1) // Обратите внимание: порядок здесь должен быть меньше, чем порядок TX: Annotation-управляем @Around ("выполнение (*abc*.controller.*.*(..)") открытый объект DoAround (TractingJoinpoint jp) бросает {servletRequestattributes sra = (ServletRequestattributes) requestContextholder.getRequestTtributes (); Httpservletrequest request = sra.getRequest (); Httpservletresponse response = sra.getResponse (); String tenantkey = request.getheader ("арендатор"); // Фронт-энд должен пройти в заголовок арендатора, в противном случае 400 будет возвращен, если (! StringUtils.hastext (tenantkey)) {webutils.tohttp (response) .senderror (httpservletresponse.sc_bad_request); вернуть ноль; } log.info ("Current Andrant Key: {}", TenantKey); DataSourceContexTholder.SetDataSourceKey (TenantKey); Object result = jp.proceed (); DataSourceContextholder.cleardatasourcekey (); результат возврата; }}Суммировать
Выше приведено метод реализации динамической регистрации пружины нескольких источников данных, введенных редактором. Я надеюсь, что это будет полезно для всех. Если у вас есть какие -либо вопросы, пожалуйста, оставьте мне сообщение, и редактор ответит всем вовремя. Большое спасибо за вашу поддержку сайту wulin.com!