Baru -baru ini, kami sedang mengerjakan aplikasi SaaS. Database mengadopsi satu instance arsitektur multi-skema (lihat referensi 1 untuk detail). Setiap penyewa memiliki skema independen, dan seluruh sumber data memiliki skema bersama, sehingga perlu untuk menyelesaikan masalah penambahan dan penghapusan dinamis dan beralih sumber data.
Setelah mencari banyak artikel secara online, banyak dari mereka berbicara tentang konfigurasi sumber data master-slave, atau mereka telah menentukan konfigurasi sumber data sebelum aplikasi dimulai, dan jarang berbicara tentang bagaimana memuat sumber data secara dinamis tanpa dimatikan, jadi saya menulis artikel ini untuk referensi.
Teknik yang digunakan
Ide
Ketika sebuah permintaan masuk, tentukan penyewa yang menjadi milik pengguna saat ini, dan beralih ke sumber data yang sesuai berdasarkan informasi penyewa, dan kemudian melakukan operasi bisnis berikutnya.
Implementasi Kode
TenantConfigentity (Informasi Penyewa) @EqualSandHashCode (calluper = false)@data@fieldeDefaults (level = accesslevel. / ***Nama penyewa **/ string tenantname; / ***Kunci Nama Penyewa **/ String TenantKey; / ***URL Database **/ String DBURL; / ***nama pengguna basis data **/ string dbuser; / ***kata sandi basis data **/ string dbpassword; / ***database public_key **/ string dbpublickey;} dataSourceutil (kelas alat Aidant, non-esensial) DataSourceutil {private static final string data_source_bean_key_suffix = "_data_source"; Private Static Final String JDBC_URL_ARGS = "? UseUnicode = true & characterencoding = UTF-8 & UseOldAliasmetadatabehavior = True & ZerodateTimebehavior = Converttonull"; Private Static Final String Connection_Properties = "config.decrypt = true; config.decrypt.key ="; / ** * Kunci kacang pegas untuk menyambung sumber data */ public static string getDataSourceBeANKey (string tenantkey) {if (! Stringutils.hastext (tenantKey)) {return null; } return tenantkey + data_source_bean_key_suffix; } / ** * splicing lengkap jdbc url * / public static string getjdbcurl (string baseurl) {if (! Stringutils.hastext (baseurl)) {return null; } return baseUrl + jdbc_url_args; } / *** Properti koneksi druid lengkap yang disambung* / string statis public getConnectionProperties (string publicKey) {if (! Stringutils.hastext (publicKey)) {return null; } return connection_properties + publicKey; }}DataSourCeContExtholder
Gunakan ThreadLocal untuk menyimpan nama kunci sumber data dari utas saat ini dan mengimplementasikan metode set, dapatkan, dan jelas;
DataSourCeContExTholder kelas publik {private static final threadlocal <string> DataSourCekey = NEW OTERITABLETHREADLOCAL <> (); public static void setDataSourCekey (string tenantkey) {DataSourCey.set (TenantKey); } public static string getDataSourCeye () {return DataSourCekey.get (); } public static void clearEdataSourCekey () {DataSourCey.Remove (); }}DynamicDataSource (titik kunci)
Mewarisi abstractroutingDataSource (disarankan untuk membaca kode sumbernya untuk memahami proses pengalihan sumber data secara dinamis) dan mewujudkan pemilihan dinamis sumber data;
Public Class DynamicDataSource memperluas abstractroutingDataSource {@Autowired private applicationContext ApplicationContext; @Lazy @Autowired Private DynamicDataSourcesummoner Summoner; @Lazy @Autowired private tenantconfigdao tenantconfigdao; @Override Protected String DetectionecurrentLookUpkey () {string tenantKey = DATASOURCECONTEXTHOLDER.GetDataSourCeye (); return DataSourceutil.getDataSourceBeankey (TenantKey); } @Override Protected DataSource detecureTargetDataSource () {string tenantKey = DataSourCeContExtholder.getDataSourCeye (); String beandey = DataSourceutil.getDataSourceBeankey (TenantKey); if (! stringutils.hastext (tenantkey) || applicationContext.containsbean (beandey)) {return super.determinetargetDataSource (); } if (tenantConfigdao.exist (tenantkey)) {summoner.registerdynamicDataSources (); } return super.determinetargetDataSource (); }}DynamicDataSourcesummoner (titik utama fokus)
Muat informasi sumber data dari database dan secara dinamis merakit dan mendaftar kacang pegas.
@Slf4j@componentPublic kelas DinamicDataSourcesumMoner mengimplementasikan ApplicationListener <contextrefreshedEvent> {// konsisten dengan ID sumber data default dari spring-data-source.xml private static string default_data_source_bean_key = "defaultDataSource"; @Autowired Private ConfigurableApplicationContext ApplicationContext; @Autowired Private DynamicDataSource DynamicDataSource; @Autowired private tenantconfigdao tenantconfigdao; private static boolean dimuat = false; / *** Jalankan setelah pemuatan pegas selesai*/ @Override public void onApplicationEvent (contextrefreshedEvent peristiwa) {// Cegah eksekusi berulang jika (! Dimuat) {loaded = true; coba {registerdynamicDataSources (); } catch (Exception e) {log.error ("Inisialisasi sumber data gagal, pengecualian:", e); }}}} / *** Baca konfigurasi DB penyewa dari database dan secara dinamis menyuntikkan wadah musim semi* / public void registerdynamicDataSources () {// Dapatkan konfigurasi db untuk semua daftar penyewa <EvenConfigentity> TenantConfigentities = TenantConfigDAo.listall (); if (collections.isempty (tenantConfigentities)) {lempar baru ilegalstateException ("Inisialisasi aplikasi gagal, harap konfigurasikan sumber data terlebih dahulu"); } // Daftarkan kacang sumber data di addDataSourceBeans (tenantConfigentities); } / *** Buat kacang berdasarkan sumber data dan daftarkan dalam wadah* / private void addDataSourceBeans (daftar <denantConfigentity> tenantConfigentities) {peta <objek, objek> targetDataSources = maps.newlinkedHashMap (); DefaultListableBeanFactory beanFactory = (defaultListableBeanFactory) applicationContext.getAutoWireCableBeanFactory (); untuk (tenantConfigentity entitas: tenantConfigentities) {string beathey = datasourceutil.getDataSourceBeANKey (entity.gettenantkey ()); // Jika sumber data telah terdaftar di musim semi, jangan mendaftar ulang jika (applicationContext.containsbean (beandey)) {druidDataSource ada axistsdataSource = applicationContext.getBean (beandey, druiddataSource.class); if (isSamedataSource (existsdataSource, entitas)) {lanjutan; }} // kumpulkan bean abstractBeandefinition beandefinition = getBeandefinition (entitas, beandey); // daftarkan bean beanfactory.registerbeandefinition (beessey, beandefinition); - } // Atur objek peta yang dibuat ke TargetDataSources; DynamicDataSource.SetTargetDataSources (TargetDataSources); // Operasi ini harus dilakukan sebelum abstractroutingDataSource akan diulangi diselesaikan dengan cara ini, hanya switching dinamis yang akan berlaku DynamicDataSource.AfterPropertiesset (); } / ** * Merakit Sumber Data Bean Spring * / Private AbstractBeandFinition GetBeandefinition (TenantConfigentity Entity, String Beanskey) {BeandefinitionBuilder Builder = beandefinitionBuilder.genericbeandefinition (druiddatasource); builder.getBeandefinition (). setAttribute ("id", be merekkey); // konfigurasi lain mewarisi DefaultDataSource builder.setParentName (default_data_source_bean_key); builder.setinitmethodname ("init"); builder.setDestroymethodname ("tutup"); builder.addpropertyvalue ("name", beankey); builder.addPropertyValue ("url", datasourceutil.getjdbcurl (entity.getDburl ())); builder.addPropertyValue ("nama pengguna", entity.getDbuser ()); builder.addPropertyValue ("kata sandi", entity.getDbpassword ()); builder.addPropertyValue ("ConnectionProperties", DataSourceutil.getConnectionProperties (entity.getDbpublickey ())); return builder.getBeandefinition (); } /** * Determine whether the DataSource in the Spring container is consistent with the DataSource information of the database* Note: There is no judgment here on public_key, because the other three information can basically be determined to be unique*/ private boolean isSameDataSource(DruidDataSource existsDataSource, TenantConfigEntity entity) { boolean sameUrl = Objects.equals (existsdataSource.getUrl (), datasourceutil.getjdbcurl (entity.getDburl ())); if (! SameUrl) {return false; } boolean soreUser = objects.equals (existsdataSource.getusername (), entity.getDbuser ()); if (! SameUser) {return false; } coba {string decryptpassword = configTools.decrypt (entity.getDbpublickKey (), entity.getDbpassword ()); return objects.equals (existsdataSource.getPassword (), decryptpassword); } catch (Exception e) {log.Error ("Verifikasi kata sandi sumber data gagal, pengecualian: {}", e); mengembalikan false; }}}Spring-Data-Source.xml
<!-Memperkenalkan file konfigurasi JDBC-> <konteks: properti-placeHolder location = "classpath: data.properties" abaikan-tidak dapat diselesaikan = "true"/> <!-sumber data publik (default)-> <bean id = "DefaultDataSource" init-method = "init" dashing-method = "close"> <! value = "$ {ds.jdbcurl}" /> <name properti = "nama pengguna" value = "$ {ds.user}" /> <name properti = "kata sandi" value = "$ {ds.password}" /> <!-Konfigurasikan ukuran inisialisasi, minimum, dan maksimum-> <nama properti = "value" value = "5" 5 "Nilai" 5 " Name = "MaxActive" value = "10" /> <!-Mengkonfigurasi waktu untuk menunggu koneksi ke batas waktu dalam milidetik-> <nama properti = "maxwait" value = "1000" /> <!-Konfigurasi berapa lama waktu yang dibutuhkan untuk mendeteksi waktu yang dibutuhkan untuk ditutup pada milideskonds-> <Properti Nama = "waktu yang dibutuhkan-berapa lama. Mendeteksi sekali, mendeteksi koneksi idle yang perlu ditutup dalam milidetik-> <nama properti = "timebetweenevictionrunsmillis" value = "5000" /> <!-Konfigurasi berapa lama untuk mendeteksi sekali, mendeteksi koneksi idle yang perlu ditutup di milides-"name properten =" waktu-waktu antara waktu tayang. Untuk bertahan di kumpulan, dalam milidetik-> <nama properti = "MinEvictableIdletimeMillis" value = "240000" /> <nama properti = "validationQuery" value = "Pilih 1" /> <!-unit: detik, waktu tunggu untuk mendeteksi apakah koneksi valid-> <nama properti = "validasi waktu validasi waktu" bernilai "60. Saat mengajukan permohonan koneksi, jika waktu idle lebih besar dari timebetweenevictionrunsmillis, lakukan validationquery untuk mendeteksi apakah koneksi valid-> <nama properti = "testwhileidle" value = "true" /> <!-jalankan validasi untuk mendeteksi apakah koneksi tersebut valid ketika berlaku untuk koneksi. Konfigurasi ini akan mengurangi kinerja. -> <name properti = "testonborrow" value = "true" /> <!-Jalankan ValidationQuery Saat mengembalikan koneksi untuk memeriksa apakah koneksi valid. Melakukan konfigurasi ini akan mengurangi kinerja. -> <name properti = "testonReturn" value = "false" /> <!-config filter-> <properti name = "filter" value = "config" /<name properti = "connectionProperties" value = "config.decrypt = true; config.decrypt.key = $ {ds.publickey}" /< /bean> <! name = "DataSource" ref = "multipleDataSource"/> </ bean> <!-Sumber multi-data-> <bean id = "multipleDataSource"> <nama properti = "DefaultTargetDataSource" Ref = "DefaultDataSource"/<Properti Nama = "TargetDataSources"> <defaultDataSource "/<Properti Nama =" TargetDataSources "> <defaultDataSource"/<Properti Nama = "TargetDataSources"> <defaultDataSource "/<Properti Nama =" TargetDataSources "> <defaultDataSource"/<Properti Nama = "TargetDataSources"> <defaultDataSource " value-ref = "defaultDataSource"/> </peta> </pruptent> </tact> <!-Annotation Transaction Manager-> <!-Nilai pesanan di sini harus lebih besar dari nilai pesanan dari DynamicDataSourCeaspecpecpecpectAdvice-> <tx: annotation-driven transaction-manager = "txManager" order = "2" 2 "2" 2 " <bean id="mainSqlSessionFactory"> <property name="dataSource" ref="multipleDataSource"/> </bean> <!-- The package name where the DAO interface is located, Spring will automatically find the DAO under it --> <bean id="mainSqlMapper"> <property name="sqlSessionFactoryBeanName" value="mainSqlSessionFactory"/> <nama properti = "Basepackage" value = "abc*.dao"/> </tact> <bean id = "DefaultSqlSessionFactory"> <name properti = "DataSource" ref = "defaultDataSource"/> </bean> <bean Id = "default -wlmapper"> <"SQ Name =" SQLEANCEFATE "> </bean> <bean" <bean "<bean" <bean "<bean" <bean "<bean" value = "DefaultSQLSessionFactory"/> <Properti Nama = "BasePackage" Value = "ABCBase.Dal.DAO"/> </bean> <!-konfigurasi lain dihilangkan->DynamicDataSourCeaspectAdvice
Secara otomatis beralih sumber data menggunakan AOP hanya untuk referensi;
@Slf4j@aspek@component@order (1) // Harap dicatat: Pesanan di sini harus lebih kecil dari urutan TX: anotasi-digerakkan, yaitu, pertama-tama jalankan bagian DynamicDataSourCeaspectAdvice, dan kemudian jalankan bagian transaksi untuk mendapatkan @enableaspectjaUtProxy (proxytargas = proxyTargas {proxytargas = proxyTargas {proxytargasceckspeksi {proxytargasceckspeksi ’ @Around ("Eksekusi (*ABC*.Controller.*.*(..))") Objek Publik Doaround (ProsidingjoinPoint JP) melempar Throwable {servletRequestAttributes sra = (servletRequestAttributes) requestContextholder.getRequestAttributes (); HttpservletRequest request = sra.getRequest (); HttpservletResponse respons = sra.getResponse (); String tenantkey = request.getHeader ("penyewa"); // front-end harus masuk ke tajuk penyewa, jika tidak 400 akan dikembalikan jika (! Stringutils.hastext (tenantkey)) {webutils.tohttp (respons) .senderror (httpservletresponse.sc_bad_request); kembali nol; } log.info ("Kunci penyewa saat ini: {}", tenantkey); DATASOURCECONTEXTHOLDER.SetDataSourcey (TenantKey); Hasil objek = jp.proed (); DATASOURCECONTEXTHOLDER.CLEARDATASOURCEEKY (); hasil pengembalian; }}Meringkaskan
Di atas adalah metode implementasi registrasi dinamis pegas dari beberapa sumber data yang diperkenalkan oleh editor. Saya harap ini akan membantu semua orang. Jika Anda memiliki pertanyaan, silakan tinggalkan saya pesan dan editor akan membalas semua orang tepat waktu. Terima kasih banyak atas dukungan Anda ke situs web Wulin.com!