최근에, 우리는 SaaS 응용 프로그램을 연구하고 있습니다. 데이터베이스는 단일 인스턴스 멀티 스키마 아키텍처를 채택합니다 (자세한 내용은 참조 1 참조). 각 임차인은 독립적 인 스키마를 가지고 있으며 전체 데이터 소스에는 공유 스키마가 있으므로 동적 추가 및 삭제 및 스위칭 데이터 소스 문제를 해결해야합니다.
많은 기사를 온라인으로 검색 한 후 많은 기사가 마스터 슬레이브 데이터 소스 구성에 대해 이야기하거나 응용 프로그램이 시작되기 전에 데이터 소스 구성을 결정했으며 종료하지 않고 데이터 소스를 동적으로로드하는 방법에 대해 거의 이야기하지 않으므로이 기사를 참조하기 위해 작성했습니다.
사용 된 기술
아이디어
요청이 들어 오면 현재 사용자가 속한 임차인을 결정한 다음 테넌트 정보를 기반으로 해당 데이터 소스로 전환 한 다음 후속 비즈니스 운영을 수행하십시오.
코드 구현
TenantConfigentity (Tenant Information) @equalsandhashcode (Callsuper = false)@data@fieldDefaults (level = accesslevel.private) public class tenantconfigentity { / ***tenant id ** / integer tenantid; / ***테넌트 이름 **/ String TenantName; / ***테넌트 이름 키 **/ String TenantKey; / ***Database URL **/ String dburl; / ***데이터베이스 사용자 이름 **/ String dbuser; / ***데이터베이스 비밀번호 **/ String dbpassword; / ***데이터베이스 public_key **/ String dbpublickey;} dataSourceutil (Aidant Tool Class, 비 필수) 공개 클래스 DataSourceutil {private static final String data_source_bean_key_suffix = "_data_source"; 개인 정적 최종 문자열 JDBC_URL_ARGS = "? useUnicode = true & charac 개인 정적 최종 문자열 connection_properties = "config.decrypt = true; config.decrypt.key ="; / ** * 데이터 소스를 접합하기위한 스프링 빈 키 */ public static string getDatasourceBeanKey (String tenantKey) {if (! stringUtils.Hastext (tenantKey)) {return null; } return tenantkey + data_source_bean_key_suffix; } / ** * 완전한 jdbc url * / public static string getJdbcurl (String baseurl) {if (! stringUtils.hastext (baseurl)) {return null; } return baseUrl + jdbc_url_args; } / *** 완전한 드루이드 연결 속성* / public static string getConnectionProperties (String PublicKey) {if (! stringUtils.Hastext (publicKey)) {return null; } return connection_properties + publickey; }}DataSourceContexTholder
ThreadLocal을 사용하여 현재 스레드의 데이터 소스 키 이름을 저장하고 세트, GET 및 CLEAR 메소드를 구현하십시오.
공개 클래스 DataSourceContexTholder {private static final threadlocal <string> dataSourcekey = new heritableThreadlocal <> (); public static void setdatasourcekey (String tenantkey) {dataSourcekey.set (tenantkey); } public static string getDatasourcekey () {return dataSourcekey.get (); } public static void cleardatasourcekey () {dataSourcey.remove (); }}DynamicDatasource (키 포인트)
AbstractroutingDatasource를 상속합니다 (데이터 소스를 동적으로 전환하는 프로세스를 이해하기 위해 소스 코드를 읽고 데이터 소스의 동적 선택을 실현하는 것이 좋습니다.
Public Class DynamicDatasource는 AbsTractroutingDatasource를 확장합니다. @lazy @autowired private dynamicdatasourcesummoner 소환사; @lazy @autowired 개인 tenantconfigdao tenantconfigdao; @override Protected String degeinecurrentLookupkey () {String tenantkey = dataSourceContexTholder.getDatasourcekey (); return datasourceutil.getDatasourcebeankey (tenantkey); } @override Protected DataSource dectainetArgetDatasource () {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 (); } return super.determinetArgetDatasource (); }}DynamicDatasourcesumMoner (키 포인트 초점)
데이터베이스에서 데이터 소스 정보를로드하고 스프링 빈을 동적으로 조립하고 등록합니다.
@slf4j@componentpublic class dynamamicDatasourcesummoner는 ApplicationListener <contextrefreshedevent> {// spring-data-source.xml의 기본 데이터 소스 ID와 일치하는 private static final String default_data_source_bean_key = "defaultDatasource"; @autowired private configurablepplicationContext ApplicationContext; @autowired Private DynamicDatasource DynamicDatasource; @autowired 개인 tenantconfigdao tenantconfigdao; 개인 정적 부울로드 = 거짓; / *** 스프링로드가 완료된 후 실행*/ @Override public void onapplicationEvent (contextrepreshedevent 이벤트) {// 반복 실행을 방지합니다. {loaded = true; {registerDynamicDatasources (); } catch (예외 e) {log.error ( "데이터 소스 초기화 실패, 예외 :", e); }}}} / *** 데이터베이스에서 테넌트의 DB 구성을 읽고 스프링 컨테이너* / public void void registerDynamicDatasources () {// 모든 임차인 목록에 대한 DB 구성을 얻습니다. if (collectionUtils.isempty (tenantConfigentities)) {New ImperalStateException 던지기 ( "응용 프로그램 초기화 실패, 데이터 소스를 먼저 구성하십시오"); } // 컨테이너에 데이터 소스 Bean을 등록합니다. } / *** DataSource를 기반으로 Bean을 생성하고 컨테이너에 등록하십시오* / private void addDatasourcebeans (list <tenantconfigentity> tenantConfigentities) {map <개체, Object> TargetDatasources = maps.newLinkedHashMap (); defaultListableBeanFactory BeanFactory = (DefaultListableBeanFactory) ApplicationContext.GetAutowEcapableBeanFactory (); for (tenantconfigentity entity : tenantconfigentities) {String beankey = dataSourceutil.getDatasourceBeanKey (entity.getTenantKey ()); // 데이터 소스가 봄에 등록 된 경우 (ApplicationContext.containsBean (Beankey)) {druiddatasource alsdatasource = applicationcontext.getbean (Beankey, druiddatasource.class); if (issamedatasource (ExistSdatasource, Entity)) {계속; }} // Bean AbstractBeanDefinition BeanDefinition 조립 = GetBeanDefinition (Entity, Beankey); // Register bean beanfactory.registerBeanDefinition (Beankey, BeanDefinition); // 맵에 넣고 Bean 객체가 지금 바로 TargetDatasources.put (Beankey, ApplicationContext.getBean (Beankey)); } // 생성 된 맵 객체를 TargetDatasources로 설정합니다. DynamicDatasource.SetTargetDatasources (TargetDatasources); //이 작업은 ABSTRAUTINGDATASOURCE가 이런 식으로 다시 해제되기 전에 수행해야하며, 동적 스위칭 만 동적 DATASOURCE.AFTERPOPERTIESSET (); } / ** * 데이터 소스 스프링 Bean * / private acc builder.getBeanDefinition (). setAttribute ( "id", beankey); // 기타 구성을 상속받은 DEFAULTDATASOURCE BUILDER.SETPARENTNAME (DEFAULT_DATA_SORCE_BEAN_KEY); builder.setinitmethodname ( "init"); builder.setdestroymethodname ( "close"); builder.adpropertyvalue ( "이름", beankey); builder.adpropertyvalue ( "url", dataSourceutil.getJdbCurl (entity.getDburl ())); builder.adpropertyValue ( "username", entity.getDbuser ()); builder.adpropertyvalue ( "password", entity.getDbpassword ()); builder.adpropertyValue ( "ConnectionProperties", dataSourceutil.getConnectionProperties (entity.getDBPublicKey ())); return builder.getBeanDefinition (); } / *** 스프링 컨테이너의 데이터 소스가 데이터베이스의 데이터 소스 정보와 일치하는지 여부를 결정하십시오. 참고 : 다른 세 가지 정보는 기본적으로 독특한* / 개인 부울 IsSameDatasource (druiddatasource, tenantconfigentity entrity) {boolean sameurl = {boolean istonconfigentity entrity)로 결정할 수 있기 때문에 여기에 판단이 없습니다. Objects.equals (ExistSdataSource.getUrl (), DataSourceutil.getJdbcurl (entity.getDburl ())); if (! sameurl) {return false; } boolean sameuser = 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 (예외 e) {log.error ( "데이터 소스 비밀번호 검증 실패, 예외 : {}", e); 거짓을 반환합니다. }}}Spring-Data-Source.xml
<!-JDBC 구성 파일을 소개합니다-> <context : property-placeholder location = "classpath : data.properties"exure-unresolvable = "true"/> <!-public (default) 데이터 소스-> <bean id = "defaultDatasource"init-method = "init" "close" ""<! <! value = "$ {ds.jdbcurl}" /> <property name = "username"value = "$ {ds.user}" /> <속성 이름 = "password"vale = "$ {ds.password}" /> <!-구성 크기, 최대 <속성 이름 = "value = <value = <"2 ""value = ""2. 이름 = "maxactive"value = "10" /> <!-밀리 초에서 시간 초과 연결을 기다리는 시간을 구성합니다-> <속성 이름 = "maxwait"value = "1000" /> <!-밀리 초에서 닫아야하는 유휴 연결을 감지하는 데 걸리는 시간-> vallis "value ="5000 vetweenevictionmillis " 한 번 감지하고 밀리 초로 닫아야하는 유휴 연결을 감지하는 데 걸립니다-> <속성 이름 = "timebetweenevictionRunsmillis"value = "5000" /> <!-한 번 감지하는 데 걸리는 시간을 구성하고 밀리 초에서 닫아야하는 유휴 연결을 감지하는 데 걸리는 시간-> <speraty name value " /<"5000 value " />>. 밀리 초에서 수영장에서 생존하기위한 최소 시간-> <속성 이름 = "minevictableDletimemillis"value = "240000" /> <속성 이름 = "validationQuery"value = "select 1" /> <!-단위 : 연결이 유효한 지 여부를 감지하는 시간 초과 시간 ", valicationquery-time affect <! 성능을 보장합니다. 연결을 적용 할 때 유휴 시간이 TimeBetweenEvictionRunsmillis보다 큰 경우, 연결이 유효한 지 여부를 감지하기 위해 ValidationQuery를 수행하여 연결을 적용 할 때 연결이 유효한지 여부를 감지하기 위해 유효성 검사 쿼리를 실행하십시오. 이 구성은 성능을 줄입니다. -> <property name = "readonborrow"value = "true" /> <!-연결을 반환 할 때 연결이 유효한지 확인할 때 유효성 검사 쿼리를 실행하십시오. 이 구성을 수행하면 성능이 줄어 듭니다. -> <property name = "testOnreturn"value = "false" /> <!-구성 필터-> <속성 이름 = "필터"value = "config" /<property name = "connectionProperties"value = "config.decrypt = true; config.decrypt.key = $ {ds.publickey}" /> <!-transactamption manager-> <bean id id = id id id id id id id id id id id id id id and <tx < /bean> < 이름 = "dataSource"ref = "multipledatasource"/</bean> <!-멀티 데이터 소스-> <bean id = "multipledatasource"> <property name = "defaultTargetDatasource"ref = "defaultDatasource"/<property name = "targetdatasources"<map> <entry key = "defaultDatasource value-Ref = "defaultdatasource"/> </map> </property> </bean> <!-주석 거래 관리자-> <!-여기의 순서 값은 DynamicDatasourCeaspecePectAdvice의 순서 값보다 커야합니다. <TX : annotation-driven-manager = "txmanager"2 "/> <! id = "mainsqlsessionfactory"> <property name = "dataSource"ref = "multipledatasource"/> </bean> <!-DAO 인터페이스가 위치한 패키지 이름, 스프링은 자동으로 DAO를 자동으로 찾을 것입니다-> <bean id = "mainsqlmapper"> <propertybeanname "values ="mainsqlsession "" "mainsqlsession" " 이름 = "basePackage"value = "abc*.dao"/> </bean> <bean id = "defaultsqlsessionfactory"> <속성 이름 = "dataSource"ref = "defaultDatasource"/> </bean> <bean id = "defaultsqlmapper"> 이름 = "BasePackage"value = "abcbase.dal.dao"/> </bean> <!-기타 구성 생략->DynamicDatasourCeaspectAdvice
참조 용 AOP를 사용하여 데이터 소스를 자동으로 전환합니다.
@slf4j@component@component@order (1) // 참고 : 여기서 주문은 TX의 순서보다 작아야합니다. : 먼저 DynamicDatasourCeaspecePecepAdvice 섹션을 실행 한 다음 거래 섹션을 실행하여 최종 데이터 소스를 실행하여 @enableaspectautoproxy (proxytargetcclass = true) 최종 데이터 소스를 얻습니다. @Around ( "execution (*abc*.controller.*.*(..))") public object doaround (proceedingjoinpoint jp) 던지기 가능 {servletrequestattributes sra = (servletrequestattributes) requestContexTholder.getRequestattributes (); httpservletrequest request = sra.getRequest (); httpservletresponse 응답 = sra.getResponse (); 문자열 tenantkey = request.getheader ( "Tenant"); // 프론트 엔드는 테넌트 헤더로 전달해야합니다. 그렇지 않으면 400이 반환됩니다. 널 리턴; } log.info ( "현재 테넌트 키 : {}", tenantkey); DataSourceContexTholder.SetDatasourcekey (TenantKey); 객체 결과 = jp.proceed (); DataSourceContexTholder.ClearDatasourcekey (); 반환 결과; }}요약
위는 편집기가 도입 한 여러 데이터 소스의 Spring Dynamic 등록의 구현 방법입니다. 모든 사람에게 도움이되기를 바랍니다. 궁금한 점이 있으면 메시지를 남겨 주시면 편집자가 제 시간에 모든 사람에게 답장을 드리겠습니다. Wulin.com 웹 사이트를 지원해 주셔서 대단히 감사합니다!