เมื่อเร็ว ๆ นี้เรากำลังทำงานกับแอปพลิเคชัน SaaS ฐานข้อมูลใช้สถาปัตยกรรมหลายอินสแตนซ์เดียว (ดูข้อมูลอ้างอิง 1 สำหรับรายละเอียด) ผู้เช่าแต่ละรายมีสคีมาอิสระและแหล่งข้อมูลทั้งหมดมีสคีมาที่ใช้ร่วมกันดังนั้นจึงจำเป็นต้องแก้ปัญหาการเพิ่มและการลบและการสลับแหล่งข้อมูล
หลังจากค้นหาบทความจำนวนมากออนไลน์พวกเขาหลายคนพูดคุยเกี่ยวกับการกำหนดค่าแหล่งข้อมูลหลัก-สลาฟหรือพวกเขาได้พิจารณาการกำหนดค่าแหล่งข้อมูลก่อนที่แอปพลิเคชันจะเริ่มต้นและไม่ค่อยพูดคุยเกี่ยวกับวิธีการโหลดแหล่งข้อมูลแบบไดนามิกโดยไม่ต้องปิดตัวลงดังนั้นฉันจึงเขียนบทความนี้เพื่ออ้างอิง
เทคนิคที่ใช้
ความคิด
เมื่อมีการร้องขอให้กำหนดผู้เช่าที่ผู้ใช้ปัจจุบันเป็นเจ้าของและเปลี่ยนไปใช้แหล่งข้อมูลที่สอดคล้องกันตามข้อมูลผู้เช่าจากนั้นดำเนินการดำเนินธุรกิจที่ตามมา
การใช้รหัส
TenantConfigentity (ข้อมูลผู้เช่า) @EqualSandHashCode (callyUper = false)@data@fieldDefaults (ระดับ = AccessLevel.Private) ชั้นเรียนสาธารณะผู้เช่า configentity { / ***ID ผู้เช่า ** / Integer Tenantid; / ***ชื่อผู้เช่า **/ String TenantName; / ***คีย์ชื่อผู้เช่า **/ String TenantKey; / ***URL ฐานข้อมูล **/ String dburl; / ***ชื่อผู้ใช้ฐานข้อมูล **/ สตริง dbuser; / ***รหัสผ่านฐานข้อมูล **/ สตริง dbpassword; / ***ฐานข้อมูล public_key **/ สตริง dbpublickey;} dataSourceUtil (คลาสเครื่องมือ Aidant, ไม่จำเป็น) DataSourceutil คลาสสาธารณะ {สตริงสุดท้ายคงที่ data_source_bean_key_suffix = "_data_source"; สตริงสุดท้ายคงที่ส่วนตัว JDBC_URL_ARGS = "? useUnicode = true & catreatencoding = UTF-8 & useoldaliasMetadatAdaHavior = true & zerodatetimeBehavior = Converttonull"; สตริงสุดท้ายคงที่การเชื่อมต่อ _Properties = "config.decrypt = true; config.decrypt.key ="; / ** * คีย์ถั่วสปริงสำหรับการประกบแหล่งข้อมูล */ สตริงคงที่สาธารณะ getDataSourceBeankey (String tenantkey) {ถ้า (! StringUtils.Hastext (ผู้เช่า)) {return null; } ส่งคืน tenantKey + data_source_bean_key_suffix; } / ** * splicing jdbc url * / สตริงคงที่สาธารณะ getjdbcurl (สตริง baseurl) {ถ้า (! stringutils.hastext (baseUrl)) {return null; } ส่งคืน baseUrl + jdbc_url_args; } / *** คุณสมบัติการเชื่อมต่อ DRUID ที่สมบูรณ์แบบ* / สตริงคงที่สาธารณะ getConnectionProperties (String PublicKey) {ถ้า (! StringUtils.Hastext (PublicKey)) {return null; } ส่งคืน connection_properties + publickey; -DataSourceContextholder
ใช้ ThreadLocal เพื่อบันทึกชื่อคีย์แหล่งข้อมูลของเธรดปัจจุบันและใช้วิธีการตั้งค่า GET และ CLEAR
Public Class DataSourceContextholder {Private Static Final ThreadLocal <String> dataSourceKey = ใหม่ MandleItableThreadLocal <> (); โมฆะคงที่สาธารณะ setDataSourceKey (String tenantKey) {dataSourceKey.set (tenantKey); } สตริงคงที่สาธารณะ getDataSourceKey () {return dataSourceKey.get (); } โมฆะสาธารณะคงที่ clearDataSourceKey () {dataSourceKey.Remove (); -DynamicDataSource (จุดสำคัญ)
สืบทอด abstractroutingDataSource (ขอแนะนำให้อ่านซอร์สโค้ดเพื่อทำความเข้าใจกระบวนการของการสลับแหล่งข้อมูลแบบไดนามิก) และตระหนักถึงการเลือกแหล่งข้อมูลแบบไดนามิก
Public Class DynamicDataSource ขยาย abstractroutingDataSource {@autowired Private ApplicationContext ApplicationContext; @lazy @autowired Private DynamicDataSourcesummoner Summoner; @lazy @autowired ผู้เช่าส่วนตัว CONVERFIGDAO TENANTCONFIGDAO; @Override String Protected DECININECURRENTLOOKUPKEY () {String tenantKey = DataSourceContextholder.getDataSourceKey (); ส่งคืน DataSourceutil.getDatasourceBeankey (ผู้เช่า); } @Override DataSource DataSource DeCinetArgetDataSource () {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 คลาส DynamicDataSourcesummoner ใช้ ApplicationListener <contextrefreshedEvent> {// สอดคล้องกับ ID แหล่งข้อมูลเริ่มต้นของสปริง-แหล่งจ่ายเงินส่วนตัว @autowired Private ConfigurableapplicationContext ApplicationContext; @autoWired Private DynamicDataSource DynamicDataSource; @autowired ส่วนตัวผู้เช่า configdao tenantconfigdao; บูลีนคงที่ส่วนตัวโหลด = เท็จ; / *** ดำเนินการหลังจากการโหลดฤดูใบไม้ผลิเสร็จสิ้น*/ @Override โมฆะสาธารณะ onApplicationEvent (เหตุการณ์ contextrefreshedEvent) {// ป้องกันการดำเนินการซ้ำ ๆ ถ้า (! โหลด) {loaded = true; ลอง {registerDynamicDataSources (); } catch (exception e) {log.error ("การเริ่มต้นแหล่งข้อมูลล้มเหลว, ข้อยกเว้น:", e); }}}} / *** อ่านการกำหนดค่า DB ของผู้เช่าจากฐานข้อมูลและฉีดสปริงคอนเทนเนอร์* / โมฆะสาธารณะ registerDynamicDataSources () {// รับการกำหนดค่า DB สำหรับผู้เช่าทั้งหมด <TenantConfigentity> ผู้เช่า if (collectionTils.isEmpty (ผู้เช่า configentities)) {โยนใหม่ legenalStateException ("การเริ่มต้นแอปพลิเคชันล้มเหลวโปรดกำหนดค่าแหล่งข้อมูลก่อน"); } // ลงทะเบียนถั่วแหล่งข้อมูลในคอนเทนเนอร์ addDataSourceBeans (ผู้เช่า configentities); } / *** สร้างถั่วตาม DataSource และลงทะเบียนในคอนเทนเนอร์* / โมฆะส่วนตัว addDataSourceBeans (รายการ <TenantConfigentity> ผู้เช่า configentities) {แผนที่ <วัตถุ, วัตถุ> targetDataSources = maps.newlinkedhashmap (); DefaultListableBeanFactory BeanFactory = (defaultListableBeanFactory) ApplicationContext.getAutowireCapableBeanFactory (); สำหรับ (TenantConfigentity Entity: TenantConfigentities) {String beankey = dataSourceutil.getDatasourceBeankey (entity.getTenantKey ()); // หากแหล่งข้อมูลได้รับการลงทะเบียนในฤดูใบไม้ผลิอย่าลงทะเบียนอีกครั้งหาก (ApplicationContext.containsebean (Beankey)) {DruidDataSource ExiseDataSource = ApplicationContext.getBean (beankey, druiddatasource.class); ถ้า (issamedataSource (ExisIsDatasource, entity)) {ดำเนินการต่อ; }} // ประกอบถั่ว AbstractBeanDefinition BeanDefinition = getBeanDefinition (เอนทิตี, Beankey); // ลงทะเบียน bean beanfactory.registerbeandefinition (Beankey, beandefinition); // ใส่ไว้ในแผนที่โปรดทราบว่าวัตถุถั่วถูกสร้างขึ้นตอนนี้ TargetDataSources.put (Beankey, ApplicationContext.getBean (Beankey)); } // ตั้งค่าวัตถุแผนที่ที่สร้างขึ้นเป็น targetDataSources; DynamicDataSource.SettArgetDataSources (TargetDataSources); // การดำเนินการนี้จะต้องดำเนินการก่อนที่ abstractroutingDataSource จะได้รับการแก้ไขใหม่ datedDataSources ด้วยวิธีนี้การสลับแบบไดนามิกเท่านั้นที่จะมีผลต่อ DynamicDataSource.AfterPropertiesset (); } / ** * รวบรวมแหล่งข้อมูล Spring Bean * / Private AbstractBeanDefinition GetBeanDefinition (เอนทิตี TenantConfigentity, String Beankey) {beanDefinitionBuilder Builder = beanDefinitionBuilder.GenericebeAndefinition builder.getBeanDefinition (). setAttribute ("id", beankey); // การกำหนดค่าอื่น ๆ สืบทอด defaultDataSource builder.setParentName (default_data_source_bean_key); builder.setInitMethodname ("init"); builder.setDestroyMethodname ("ปิด"); 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 (); } / *** พิจารณาว่าแหล่งข้อมูลในคอนเทนเนอร์ฤดูใบไม้ผลินั้นสอดคล้องกับข้อมูลแหล่งข้อมูลของฐานข้อมูล* หมายเหตุ: ไม่มีการตัดสินที่นี่ในที่สาธารณะ _Key เพราะข้อมูลอีกสามข้อมูลสามารถพิจารณาได้ว่าเป็นเอกลักษณ์* / บูลีนส่วนตัว Objects.equals (ExistsDataSource.getUrl (), DataSourceUtil.getJdBcurl (entity.getDburl ())); if (! sameUrl) {return false; } Boolean SameUser = Objects.exals (ExistsDataSource.getUserName (), entity.getDbuser ()); if (! SameUser) {return false; } ลอง {String DecryptPassword = configtools.decrypt (entity.getDbPublicKey (), entity.getDbPassword ()); return objects.equals (ExistsDataSource.getPassword (), DecryptPassword); } catch (Exception e) {log.error ("การตรวจสอบรหัสผ่านแหล่งข้อมูลล้มเหลว, ข้อยกเว้น: {}", e); กลับเท็จ; -Spring-Data-source.xml
<!-แนะนำไฟล์การกำหนดค่า JDBC-> <บริบท: สถานที่ตั้งของผู้ถือครองตำแหน่ง = "classpath: data.properties" ละเว้น-unresolvable = "true"/> <! value = "$ {ds.jdbcurl}" /> <property name = "username" value = "$ {ds.user}" /> <property name = "รหัสผ่าน" value = "$ {ds.password}" /> <! name = "maxactive" value = "10" /> <!-กำหนดค่าเวลาที่จะรอการเชื่อมต่อกับการหมดเวลาในมิลลิวินาที-> <ชื่อคุณสมบัติ = "maxwait" value = "1000" /> <! ตรวจจับครั้งเดียวตรวจจับการเชื่อมต่อที่ไม่ได้ใช้งานที่ต้องปิดในมิลลิวินาที-> <ชื่อคุณสมบัติ = "timebetweenevictionrunsmillis" value = "5000" /> <!-กำหนดค่าใช้จ่ายนานแค่ไหนในการตรวจจับการเชื่อมต่อที่ไม่ได้ใช้งาน เพื่อความอยู่รอดในสระว่ายน้ำในมิลลิวินาที-> <property name = "minevictableidletItimeLis" value = "240000" /> <name property = "validationQuery" value = "เลือก 1" /> <! เมื่อใช้สำหรับการเชื่อมต่อหากเวลาว่างมากกว่า timebetweenevictionrunsmillis ให้ทำการตรวจสอบความถูกต้องเพื่อตรวจสอบว่าการเชื่อมต่อนั้นถูกต้อง-> <ชื่อคุณสมบัติ = "testharyidle" value = "true" /> <! การกำหนดค่านี้จะลดประสิทธิภาพ -> <property name = "testOnBorrow" value = "true" /> <!-ดำเนินการ ValidationQuery เมื่อส่งคืนการเชื่อมต่อเพื่อตรวจสอบว่าการเชื่อมต่อนั้นถูกต้องหรือไม่ การกำหนดค่านี้จะลดประสิทธิภาพ -> <property name = "testonReturn" value = "false" /> <!-config filter-> <property name = "filters" value = "config" /> <property name = "connectionProperties" value = "config.decrypt = true; config.decrypt.key = $ {ds.publickey} name = "dataSource" ref = "multipleDataSource"/> </ebean> <!-แหล่งข้อมูลหลายแผ่น-> <bean id = "multipleDataSource"> <property name = "defaultTargetDataSource" ref = "defaultDataSource"/> value-ref = "defaultDataSource"/> </perty> </property> </ebean> <!-ตัวจัดการธุรกรรมคำอธิบายประกอบ-> <!-ค่าการสั่งซื้อที่นี่จะต้องมากกว่าค่าลำดับของ DynamicDataSourceAspectIdvice-> <tx: การทำรายการ id = "MainSqlSessionFactory"> <property name = "dataSource" ref = "multipleDataSource"/> </ebean> <!-ชื่อแพ็คเกจที่อินเทอร์เฟซ DAO ตั้งอยู่สปริงจะค้นหา DAO โดยอัตโนมัติ-> <bean iD = "Mainsqlmapper" name = "basepackage" value = "abc*.dao"/> </ebean> <bean id = "defaultSqlSessionFactory"> <property name = "dataSource" ref = "defaultDataSource"/> </epoannaTfactions name = "basepackage" value = "abcbase.dal.dao"/> </ebean> <!-การกำหนดค่าอื่น ๆ ที่ถูกละเว้น->DynamicDataSourceAspectAdvice
สลับแหล่งข้อมูลโดยอัตโนมัติโดยใช้ AOP สำหรับการอ้างอิงเท่านั้น
@SLF4J@ASPACT@Component@order (1) // โปรดทราบ: คำสั่งซื้อที่นี่จะต้องน้อยกว่าลำดับของ TX: คำอธิบายประกอบที่ขับเคลื่อนนั่นคือการเรียกใช้ส่วนแรกของ DynamicDataSourceSpectishAdvice จากนั้นดำเนินการในการทำธุรกรรม @Around ("การดำเนินการ (*abc*.controller.*.*(.. ))") วัตถุสาธารณะ doaround (ดำเนินการ jppoint jp) โยนได้ {servletrequestattributes sra = (servletrequestattributes) requestcontextholder.getRequestattributes (); httpservletRequest Request = sra.getRequest (); httpservletResponse response = sra.getResponse (); String TenantKey = request.getheader ("ผู้เช่า"); // front-end จะต้องผ่านเข้าไปในส่วนหัวของผู้เช่ามิฉะนั้น 400 จะถูกส่งคืนถ้า (! stringutils.hastext (tenantkey)) {webutils.tohttp (การตอบสนอง) .senderror (httpservletResponse.sc_bad_request); คืนค่า null; } log.info ("คีย์ผู้เช่าปัจจุบัน: {}", ผู้เช่า); DataSourceContextholder.setDataSourceKey (TenantKey); ผลลัพธ์ของวัตถุ = jp.proceed (); DataSourceContextholder.ClearDataSourceKey (); ผลการกลับมา; -สรุป
ข้างต้นเป็นวิธีการใช้งานของการลงทะเบียนแบบไดนามิกสปริงของแหล่งข้อมูลหลายแหล่งที่แนะนำโดยตัวแก้ไข ฉันหวังว่ามันจะเป็นประโยชน์กับทุกคน หากคุณมีคำถามใด ๆ โปรดฝากข้อความถึงฉันและบรรณาธิการจะตอบกลับทุกคนในเวลา ขอบคุณมากสำหรับการสนับสนุนเว็บไซต์ Wulin.com!