<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從零實(shí)現(xiàn) SpringBoot 簡(jiǎn)易讀寫分離,也不難嘛!

          共 13539字,需瀏覽 28分鐘

           ·

          2021-06-09 10:57

          作者 | 溫安適

          來源 | https://my.oschina.net/floor/blog/1632565

          最近在學(xué)習(xí)Spring boot,寫了個(gè)讀寫分離。并未照搬網(wǎng)文,而是獨(dú)立思考后的成果,寫完以后發(fā)現(xiàn)從零開始寫讀寫分離并不難!

          我最初的想法是:讀方法走讀庫,寫方法走寫庫(一般是主庫),保證在Spring提交事務(wù)之前確定數(shù)據(jù)源.

          微信新增“炸屎”功能,被好友玩壞了。。

          保證在Spring提交事務(wù)之前確定數(shù)據(jù)源,這個(gè)簡(jiǎn)單,利用AOP寫個(gè)切換數(shù)據(jù)源的切面,讓他的優(yōu)先級(jí)高于Spring事務(wù)切面的優(yōu)先級(jí)。至于讀,寫方法的區(qū)分可以用2個(gè)注解。

          但是如何切換數(shù)據(jù)庫呢?我完全不知道!多年經(jīng)驗(yàn)告訴我

          當(dāng)完全不了解一個(gè)技術(shù)時(shí),先搜索學(xué)習(xí)必要知識(shí),之后再動(dòng)手嘗試。

          我搜索了一些網(wǎng)文,發(fā)現(xiàn)都提到了一個(gè)AbstractRoutingDataSource類。查看源碼注釋如下

          /** Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
          * calls to one of various target DataSources based on a lookup key. The latter is usually
          * (but not necessarily) determined through some thread-bound transaction context.
          *
          @author Juergen Hoeller
          @since 2.0.1
          @see #setTargetDataSources
          @see #setDefaultTargetDataSource
          @see #determineCurrentLookupKey()
          */

          AbstractRoutingDataSource就是DataSource的抽象,基于lookup key的方式在多個(gè)數(shù)據(jù)庫中進(jìn)行切換。 重點(diǎn)關(guān)注setTargetDataSources,setDefaultTargetDataSource,determineCurrentLookupKey三個(gè)方法。那么AbstractRoutingDataSource就是Spring讀寫分離的關(guān)鍵了。

          仔細(xì)閱讀了三個(gè)方法,基本上跟方法名的意思一致。setTargetDataSources設(shè)置備選的數(shù)據(jù)源集合。setDefaultTargetDataSource設(shè)置默認(rèn)數(shù)據(jù)源,determineCurrentLookupKey決定當(dāng)前數(shù)據(jù)源的對(duì)應(yīng)的key。

          但是我很好奇這3個(gè)方法都沒有包含切換數(shù)據(jù)庫的邏輯啊!我仔細(xì)閱讀源碼發(fā)現(xiàn)一個(gè)方法,determineTargetDataSource方法,其實(shí)它才是獲取數(shù)據(jù)源的實(shí)現(xiàn)。源碼如下:

              //切換數(shù)據(jù)庫的核心邏輯
              protected DataSource determineTargetDataSource() {
            Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
            Object lookupKey = determineCurrentLookupKey();
            DataSource dataSource = this.resolvedDataSources.get(lookupKey);
            if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
             dataSource = this.resolvedDefaultDataSource;
            }
            if (dataSource == null) {
             throw new IllegalStateException
                        ("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
            }
            return dataSource;
           }
              //之前的2個(gè)核心方法
           public void setTargetDataSources(Map<Object, Object> targetDataSources) {
            this.targetDataSources = targetDataSources;
           }
              public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
            this.defaultTargetDataSource = defaultTargetDataSource;
           }

          簡(jiǎn)單說就是,根據(jù)determineCurrentLookupKey獲取的key,在resolvedDataSources這個(gè)Map中查找對(duì)應(yīng)的datasource!,注意determineTargetDataSource方法竟然不使用的targetDataSources!

          那一定存在resolvedDataSources與targetDataSources的對(duì)應(yīng)關(guān)系。我接著翻閱代碼,發(fā)現(xiàn)一個(gè)afterPropertiesSet方法(Spring源碼中InitializingBean接口中的方法),這個(gè)方法將targetDataSources的值賦予了resolvedDataSources。源碼如下:

           @Override
           public void afterPropertiesSet() {
            if (this.targetDataSources == null) {
             throw new IllegalArgumentException("Property 'targetDataSources' is required");
            }
            this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
            for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
             Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
             DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
             this.resolvedDataSources.put(lookupKey, dataSource);
            }
            if (this.defaultTargetDataSource != null) {
             this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
            }
           }

          afterPropertiesSet 方法,熟悉Spring的都知道,它在bean實(shí)例已經(jīng)創(chuàng)建好,且屬性值和依賴的其他bean實(shí)例都已經(jīng)注入以后執(zhí)行。

          也就是說調(diào)用,targetDataSources,defaultTargetDataSource的賦值一定要在afterPropertiesSet前邊執(zhí)行。

          AbstractRoutingDataSource簡(jiǎn)單總結(jié):

          1. AbstractRoutingDataSource,內(nèi)部有一個(gè)Map<Object,DataSource>的域resolvedDataSources
          2. determineTargetDataSource方法通過determineCurrentLookupKey方法獲得key,進(jìn)而從map中取得對(duì)應(yīng)的DataSource。
          3. setTargetDataSources 設(shè)置 targetDataSources
          4. setDefaultTargetDataSource 設(shè)置 defaultTargetDataSource,
          5. targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉(zhuǎn)換為resolvedDataSources和resolvedDefaultDataSource。
          6. targetDataSources,defaultTargetDataSource的賦值一定要在afterPropertiesSet前邊執(zhí)行。

          進(jìn)一步了解理論后,讀寫分離的方式則基本上出現(xiàn)在眼前了。(“下列方法不唯一”)

          先寫一個(gè)類繼承AbstractRoutingDataSource,實(shí)現(xiàn)determineCurrentLookupKey方法,和afterPropertiesSet方法。afterPropertiesSet方法中調(diào)用setDefaultTargetDataSource和setTargetDataSources方法之后調(diào)用super.afterPropertiesSet。

          之后定義一個(gè)切面在事務(wù)切面之前執(zhí)行,確定真實(shí)數(shù)據(jù)源對(duì)應(yīng)的key。但是這又出現(xiàn)了一個(gè)問題,如何線程安全的情況下傳遞每個(gè)線程獨(dú)立的key呢?沒錯(cuò)使用ThreadLocal傳遞真實(shí)數(shù)據(jù)源對(duì)應(yīng)的key

          ThreadLocal,Thread的局部變量,確保每一個(gè)線程都維護(hù)變量的一個(gè)副本

          到這里基本邏輯就想通了,之后就是寫了。

          DataSourceContextHolder 使用ThreadLocal存儲(chǔ)真實(shí)數(shù)據(jù)源對(duì)應(yīng)的key

          public class DataSourceContextHolder {  
              private static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
           //線程本地環(huán)境  
              private static final ThreadLocal<String> local = new ThreadLocal<String>();   
              public static void setRead() {  
                  local.set(DataSourceType.read.name());  
                  log.info("數(shù)據(jù)庫切換到讀庫...");  
              }  
              public static void setWrite() {  
                  local.set(DataSourceType.write.name());  
                  log.info("數(shù)據(jù)庫切換到寫庫...");  
              }  
              public static String getReadOrWrite() {  
                  return local.get();  
              }  
          }

          DataSourceAopAspect 切面切換真實(shí)數(shù)據(jù)源對(duì)應(yīng)的key,并設(shè)置優(yōu)先級(jí)保證高于事務(wù)切面

          @Aspect  
          @EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)  
          @Component  
          public class DataSourceAopAspect implements PriorityOrdered{

            @Before("execution(* com.springboot.demo.mybatis.service.readorwrite..*.*(..)) "  
                      + " and @annotation(com.springboot.demo.mybatis.readorwrite.annatation.ReadDataSource) ")  
              public void setReadDataSourceType() {  
                  //如果已經(jīng)開啟寫事務(wù)了,那之后的所有讀都從寫庫讀  
                      DataSourceContextHolder.setRead();    
              }  
              @Before("execution(* com.springboot.demo.mybatis.service.readorwrite..*.*(..)) "  
                      + " and @annotation(com.springboot.demo.mybatis.readorwrite.annatation.WriteDataSource) ")  
              public void setWriteDataSourceType() {  
                  DataSourceContextHolder.setWrite();  
              }  
           @Override
           public int getOrder() {
            /** 
                   * 值越小,越優(yōu)先執(zhí)行 要優(yōu)于事務(wù)的執(zhí)行 
                   * 在啟動(dòng)類中加上了@EnableTransactionManagement(order = 10)  
                   */
            
            return 1;
           }
          }

          RoutingDataSouceImpl實(shí)現(xiàn)AbstractRoutingDataSource的邏輯

          @Component
          public class RoutingDataSouceImpl extends AbstractRoutingDataSource {
           
           @Override
           public void afterPropertiesSet() {
            //初始化bean的時(shí)候執(zhí)行,可以針對(duì)某個(gè)具體的bean進(jìn)行配置
            //afterPropertiesSet 早于init-method
            //將datasource注入到targetDataSources中,可以為后續(xù)路由用到的key
            this.setDefaultTargetDataSource(writeDataSource);
            Map<Object,Object>targetDataSources=new HashMap<Object,Object>();
            targetDataSources.put( DataSourceType.write.name(), writeDataSource);
            targetDataSources.put( DataSourceType.read.name(),  readDataSource);
            this.setTargetDataSources(targetDataSources);
            //執(zhí)行原有afterPropertiesSet邏輯,
            //即將targetDataSources中的DataSource加載到resolvedDataSources
            super.afterPropertiesSet();
           }
           @Override
           protected Object determineCurrentLookupKey() {
            //這里邊就是讀寫分離邏輯,最后返回的是setTargetDataSources保存的Map對(duì)應(yīng)的key
            String typeKey = DataSourceContextHolder.getReadOrWrite();  
            Assert.notNull(typeKey, "數(shù)據(jù)庫路由發(fā)現(xiàn)typeKey is null,無法抉擇使用哪個(gè)庫");
            log.info("使用"+typeKey+"數(shù)據(jù)庫.............");  
            return typeKey;
           }
             private static Logger log = LoggerFactory.getLogger(RoutingDataSouceImpl.class)
           @Autowired  
           @Qualifier("writeDataSource")  
           private DataSource writeDataSource;  
           @Autowired  
           @Qualifier("readDataSource")  
           private DataSource readDataSource;  
          }

          基本邏輯實(shí)現(xiàn)完畢了就進(jìn)行,通用設(shè)置,設(shè)置數(shù)據(jù)源,事務(wù),SqlSessionFactory等

           @Primary
           @Bean(name = "writeDataSource", destroyMethod = "close")
           @ConfigurationProperties(prefix = "test_write")
           public DataSource writeDataSource() {
            return new DruidDataSource();
           }

           @Bean(name = "readDataSource", destroyMethod = "close")
           @ConfigurationProperties(prefix = "test_read")
           public DataSource readDataSource() {
            return new DruidDataSource();
           }

               @Bean(name = "writeOrReadsqlSessionFactory")
           public SqlSessionFactory 
                     sqlSessionFactorys(RoutingDataSouceImpl roundRobinDataSouceProxy) 
                                                                     throws Exception 
          {
            try {
             SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
             bean.setDataSource(roundRobinDataSouceProxy);
             ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
             // 實(shí)體類對(duì)應(yīng)的位置
             bean.setTypeAliasesPackage("com.springboot.demo.mybatis.model");
             // mybatis的XML的配置
             bean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
             return bean.getObject();
            } catch (IOException e) {
             log.error("" + e);
             return null;
            } catch (Exception e) {
             log.error("" + e);
             return null;
            }
           }

              @Bean(name = "writeOrReadTransactionManager")
           public DataSourceTransactionManager transactionManager(RoutingDataSouceImpl 
                        roundRobinDataSouceProxy)
           
          {
            //Spring 的jdbc事務(wù)管理器
            DataSourceTransactionManager transactionManager = new 
                            DataSourceTransactionManager(roundRobinDataSouceProxy);
            return transactionManager;
           }

          其他代碼,就不在這里贅述了,有興趣可以移步完整代碼。

          使用Spring寫讀寫分離,其核心就是AbstractRoutingDataSource,源碼不難,讀懂之后,寫個(gè)讀寫分離就簡(jiǎn)單了!。

          AbstractRoutingDataSource重點(diǎn)回顧:

          1. AbstractRoutingDataSource,內(nèi)部有一個(gè)Map<Object,DataSource>的域resolvedDataSources
          2. determineTargetDataSource方法通過determineCurrentLookupKey方法獲得key,進(jìn)而從map中取得對(duì)應(yīng)的DataSource。
          3. setTargetDataSources 設(shè)置 targetDataSources
          4. setDefaultTargetDataSource 設(shè)置 defaultTargetDataSource,
          5. targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉(zhuǎn)換為resolvedDataSources和resolvedDefaultDataSource。
          6. targetDataSources,defaultTargetDataSource的賦值一定要在afterPropertiesSet前邊執(zhí)行。

          這周確實(shí)有點(diǎn)忙,周五花費(fèi)了些時(shí)間不過總算實(shí)現(xiàn)了自己的諾言。

          完成承諾不容易,喜歡您就點(diǎn)個(gè)贊!

          瀏覽 77
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  九月丁香五月婷婷 | 在线观看自拍视频 | 亚洲 欧美 中文 日韩a v一区 | 国产精品成人在线视频 | 亚洲AV无码成人国产精品色 |