في الآونة الأخيرة ، نحن نعمل على تطبيقات SaaS. تعتمد قاعدة البيانات على مثيل واحد بنية متعددة السليم (انظر المرجع 1 للحصول على التفاصيل). يحتوي كل مستأجر على مخطط مستقل ، ويحتوي مصدر البيانات بأكمله على مخطط مشترك ، لذلك من الضروري حل مشكلة الإضافة الديناميكية وحذف مصادر البيانات.
بعد البحث عن العديد من المقالات عبر الإنترنت ، يتحدث الكثير منها عن تكوين مصدر البيانات الرئيسي ، أو قاموا بتحديد تكوين مصدر البيانات قبل بدء التطبيق ، ونادراً ما يتحدثون عن كيفية تحميل مصدر البيانات ديناميكيًا دون الإغلاق ، لذلك كتبت هذه المقالة كمرجع.
التقنيات المستخدمة
الأفكار
عندما يأتي الطلب ، حدد المستأجر الذي ينتمي إليه المستخدم الحالي ، والتحول إلى مصدر البيانات المقابل استنادًا إلى معلومات المستأجر ، ثم إجراء عمليات تجارية لاحقة.
تنفيذ الكود
TenantConfigentity (معلومات المستأجر) equalsandhashcode (callsuper = false)@data@fielddefaults (level = accesslevel.private) الفئة العامة tenantconfigentity { / ***معرف المستأجر ** / integer tenantid ؛ / ***اسم المستأجر **/ String TenantName ؛ / ***مفتاح اسم المستأجر **/ String Tenantkey ؛ / ***URL DATABASE **/ String dBURL ؛ / ***database اسم المستخدم **/ سلسلة dbuser ؛ / ***كلمة مرور قاعدة البيانات **/ String dbPassword ؛ / ***قاعدة بيانات public_key **/ string dbpublickey ؛} dataSourceUtil (فئة أداة Aidant ، غير ضرورية) dataSourceUtil {private static final string data_source_bean_key_suffix = "_data_source" ؛ Static Final Final String JDBC_URL_ARGS = "؟ UseUnicode = true & directionDing = UTF-8 & UseoldaliasMetAdataBehavior = true & ZerodateTimeBehavior = Confernull" ؛ static final string connection_properties = "config.decrypt = true ؛ config.decrypt.key =" ؛ / ** * مفتاح Bean Spring لربط مصادر البيانات */ سلسلة ثابتة عامة getDataSourceBeanKey (String tenantkey) {if (! stringutils.hastext (tenantkey)) {return null ؛ } إرجاع tenantkey + data_source_bean_key_suffix ؛ } / ** * الربط الكامل JDBC url * / سلسلة ثابتة عامة getjdbcurl (سلسلة baseurl) {if (! stringUtils.hastext (baseurl)) {return null ؛ } return baseurl + jdbc_url_args ؛ } / *** خصائص اتصال Druid الكاملة* / سلسلة ثابتة عامة getConnectionProperties (String publicKey) {if (! stringUtils.hastext (publickey)) {return null ؛ } return connection_properties + publickey ؛ }}DataSourCeContextholder
استخدم ThreadLocal لحفظ اسم مفتاح مصدر البيانات للمعلومات الحالية وتنفيذ الأساليب والحصول عليها ومسحها ؛
الفئة العامة dataSourCeContextholder {private static final threadlocal <string> dataSourceKey = new HerietAblethReadLocal <> () ؛ public static void setDataSourceKey (String tenantkey) {datasourceKey.set (tenantkey) ؛ } سلسلة ثابتة عامة getDataSourceKey () {return datasourceKey.get () ؛ } public static void clearDataSourceKey () {datasourceKey.Remove () ؛ }}DynamicDataSource (النقطة الرئيسية)
وراثة abrtractractroutingdataSource (يوصى بقراءة رمز المصدر الخاص به لفهم عملية تبديل البيانات ديناميكيا) وتحقيق الانتقاء الديناميكي لمصادر البيانات ؛
الطبقة العامة DynamicDataSource يمتد AbstRactRoutingDataSource {AuTowired Private ApplicationContext ApplicationContext ؛ @lazyautowired dynamicDataSourcesummoner Summoner ؛ @lazyautowired الخاص tenantConfigdao TenantConfigdao ؛ Override محمية السلسلة DETERNECURRENTOKTOKEUPKEY () {String tenantkey = datasourCeContextholder.getDataSourceKey () ؛ إرجاع dataSourceUtil.getDatasourceBeankey (Tenantkey) ؛ } Override DataSource DETERSINETARGETDATASOURCE () {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 dynamicdataSourcesummoner تنفذ applicationlistener <IntextreFreshedEvent> {// بما يتوافق مع معرف مصدر البيانات الافتراضي لـ spring-data-source.xml private static string string default_data_source_bean_key = "defaultdatasource" ؛ Autowired Private ConfigurableApplicationContext ApplicationContext ؛ Autowired DynamicDataSource DynamicDataSource ؛ AUTOWIRED الخاص TENTANTCONFIGDAO TENANTCONFIGDAO ؛ تم تحميل منطقية ثابتة خاصة = خطأ ؛ / *** تنفيذ بعد الانتهاء من تحميل الربيع*/ Override public void onapplicationEvent (intextrefreshedevent حدث) {// منع التنفيذ المتكرر إذا (! تحميل) {loaded = true ؛ حاول {registerDynamicDataSources () ؛ } catch (استثناء e) {log.error ("فشل تهيئة مصدر البيانات ، استثناء:" ، e) ؛ }}}} / *** اقرأ تكوين DB للمستأجر من قاعدة البيانات وحقن حاوية الربيع* / public void registerDynamicDataSources () {// الحصول على تكوين DB لجميع المستأجرين قائمة <TenantConfigentity> TenantConfigentities = ennantconfigda.listall () ؛ if (collectionUtils.isempty (TenantConfigentities)) {رمي جديد غير aluvalstateException ("فشل تهيئة التطبيق ، يرجى تكوين مصدر البيانات أولاً") ؛ }. } / *** إنشاء فاصوليا استنادًا إلى DataSource والتسجيل في الحاوية* / private void addDataSourceBeans (قائمة <TenantConfigentity> TenantConfigentities) {map <object ، targetdataSources = maps.newlinkedhashmap () ؛ DefaultListableBeanfactory Beanfactory = (DefaultListableBeanFactory) ApplicationContext.getAutowIreCableBeanfactory () ؛ لـ (endantconfigentity untity: tenantconfigentities) {String beankey = dataSourceUtil.getDataSourceBeanKey (entity.gettenankekey ()) ؛ // إذا تم تسجيل مصدر البيانات في فصل الربيع ، فلا تعيد التسجيل إذا (ApplicationContext.containsbean (Beankey)) {druiddataSource arexdataSource = ApplicationContext.getBean (Beankey ، DruidDatasource.Class) ؛ if (issameDataSource (arexdataSource ، untity)) {contern ؛ }} // Assemble Bean AbstractBeanDefinition BeanDefinition = getBeanDefinition (الكيان ، Beankey) ؛ // تسجيل beanfactory.registerBeanDefinition (Beankey ، BeanDefinition) ؛ // ضعها في الخريطة ، لاحظ أنه تم إنشاء كائن Bean الآن TargetDataSources.put (Beankey ، ApplicationContext.getBean (Beankey)) ؛ } // قم بتعيين كائن الخريطة الذي تم إنشاؤه إلى TargetDataSources ؛ DynamicDataSource.SetTargetDataSources (TargetDataSources) ؛ // يجب تنفيذ هذه العملية قبل أن يتم إعادة صياغة ResolvedDataSources abstractractroutDataSource بهذه الطريقة ، وسيؤثر فقط التبديل الديناميكي على DynamicDataSource.afterPropertiesset () ؛ } / ** * تجميع مصدر البيانات الربيع بين * / private abstractbeandefinition getBeanDefinition (endantconfigentity ictity ، 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 ("name" ، Beankey) ؛ builder.addpropertyvalue ("url" ، dataSourceUtil.getJdBcurl (entity.getDburl ())) ؛ builder.addpropertyvalue ("اسم المستخدم" ، entity.getdbuser ()) ؛ builder.addpropertyvalue ("كلمة المرور" ، entity.getDbassword ()) ؛ Builder.AddPropertyValue ("ConnectionProperties" ، dataSourceUtil.getConnectionProperties (entity.getDbpublicKey ())) ؛ إرجاع builder.getBeanDefinition () ؛ } / *** حدد ما إذا كان مصدر البيانات في حاوية الربيع يتوافق مع معلومات مصدر البيانات الخاص بقاعدة البيانات* ملاحظة: لا يوجد حكم هنا على public_key ، لأن المعلومات الثلاثة الأخرى يمكن تحديدها على أنها فريدة من نوعها* / private boolean issamedataSource (druiddataSource Objects.equals (arexdatasource.geturl () ، dataSourceUtil.getJdbCurl (entity.getDburl ())) ؛ if (! sameurl) {return false ؛ } boolean sameuser = objects.equals (arexdatasource.getUserName () ، entity.getDbuser ()) ؛ if (! sameuser) {return false ؛ } جرب {string decryptpassword = configtools.decrypt (entity.getDBpublicKey () ، entity.getDbassword ()) ؛ إرجاع objects.equals (arexdatasource.getPassword () ، decryptpassword) ؛ } catch (استثناء e) {log.error ("فشل التحقق من كلمة مرور مصدر البيانات ، الاستثناء: {}" ، e) ؛ العودة كاذبة }}}الربيع data-source.xml
<!-تقديم ملف تكوين JDBC-> <السياق: property-placeholder location = "classpath: data.properties" تجاهل-unresolvable = "true"/> <!-public (الافتراضي) مصدر البيانات-> <bean id = "revaultDatasource" init-method = "init-method =" close " value = "$ {ds.jdbcurl}" /> <property name = "username" value = "$ {ds.user}" /> <property name = "password" value = "$ {ds.password}" /> <!-تكوين التهيئة ، الحد الأدنى ، والحد الأقصى-> name = "maxactive" value = "10" /> <!-تكوين الوقت للانتظار للاتصال بـ Timeout بالمللي ثانية-> <property name = "maxwait" value = "1000" /> <!-تكوين المدة التي يستغرقها الكشف عن اتصال الخمول الذي يجب إغلاقه بالملليسيكوند-> اكتشف مرة واحدة ، اكتشف اتصال الخمول الذي يجب إغلاقه بالمللي ثانية-> <اسم property = "timeBetweenevictionRunsMillis" value = "5000" /> <!-تكوين المدة التي يستغرقها الكشف مرة واحدة ، اكتشف اتصال الخمول الذي يجب إغلاقه في Milliseconds-> للبقاء على قيد الحياة في المجمع ، في milliseconds-> <property name = "MineVictableDletimEmillis" value = "240000" /> <property name = "ValidationQuery" value = "select 1" /> <! عند التقدم بطلب للحصول على اتصال ، إذا كان وقت الخمول أكبر من TimeBetweenevictionRunsMillis ، فقم بإجراء التحقق من الصحة لاكتشاف ما إذا كان الاتصال صالحًا-> <property name = "testwhileIdle" value = "true" /> <!-تنفيذ التحقق من الصحة لاكتشاف ما إذا كان الاتصال صالحًا عند التقدم للاتصال. سيؤدي هذا التكوين إلى تقليل الأداء. -> <property name = "testOnborrow" value = "true" /> <!-تنفيذ التحقق من الصحة عند إرجاع الاتصال للتحقق مما إذا كان الاتصال صالحًا. القيام بهذا التكوين سيقلل من الأداء. -> <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"/> </bean> <!-multi-data source-> <bean id = "multipledatasource"> <property name = "defaultTargetDataSource value-ref = "defaultDataSource"/> </map> </property> </bean> <!-مدير معاملات التعليقات التوضيحية-> <!-يجب أن تكون قيمة الطلب هنا أكبر من قيمة ترتيب DynamicDataSourCeaseRcepticeDvice-> <tx: إنشاء DATERATION-RAVERATION-RAVERATION = "txmanager" <bean id = "mainsqlsessionfactory"> <property name = "datasource" ref = "multipledataSource"/> </bean> <!-اسم الحزمة حيث توجد واجهة DAO ، ستجد Spring تلقائيًا DAO Under It-> <bean id = "mainsqlmapper"> <propert name = "basePackage" value = "abc*.dao"/> </bean> <bean id = "defaultSqlSessionFactory"> <property name = "dataSource" ref = "defaultDataSource"/> </bean> <bean id = "defaultSqlMapper"> <property name = sqlsActoryFactoryNAME <property name = "basePackage" value = "abcbase.dal.dao"/> </bean> <!-تم حذف التكوين الآخر->DynamicDataSourCeaspectAdvice
تبديل مصادر البيانات تلقائيًا باستخدام AOP للرجوع إليه فقط ؛
@slf4j@side@component@order (1) // يرجى ملاحظة: يجب أن يكون الترتيب هنا أقل من ترتيب TX: يحركه التعليقات التوضيحية ، أي أولاً تنفيذ قسم DynamicDataSourCeaseventAdvice ، ثم تنفيذ قسم المعاملات damplicataRdAtaRsaRaStArdAtArdAtArdAtArdAtArdAtaRdAtaRseviceRxy (proxytargetClass = true) public. around ("التنفيذ (*ABC*. طلب httpservletrequest = sra.getRequest () ؛ httpservletresponse استجابة = sra.getResponse () ؛ String tenantkey = request.getheader ("المستأجر") ؛ // يجب أن تمر الواجهة الأمامية إلى رأس المستأجر ، وإلا سيتم إرجاع 400 إذا (! stringUtils.hastext (tenantkey)) {WebUtils.tohttp (response) .senderror (httpservletresponse.sc_bad_request) ؛ العودة لاغية. } log.info ("مفتاح المستأجر الحالي: {}" ، tenantkey) ؛ DatasourCeContextholder.setDatasourceKey (Tenantkey) ؛ نتيجة الكائن = jp.proceed () ؛ datasourceContextholder.cleardatasourceKey () ؛ نتيجة العودة }}لخص
ما سبق هو طريقة تنفيذ التسجيل الديناميكي لمصادر البيانات المتعددة التي أدخلها المحرر. آمل أن يكون ذلك مفيدًا للجميع. إذا كان لديك أي أسئلة ، فيرجى ترك رسالة لي وسوف يرد المحرر على الجميع في الوقت المناسب. شكرا جزيلا لدعمكم لموقع wulin.com!