從 0 開始寫了個讀寫分離,不難嘛!

寫了個讀寫分離,並未照搬網文,而是獨立思考後的成果,寫完之後發現從零開始寫讀寫分離並不難!
java

我最初的想法是:讀方法走讀庫,寫方法走寫庫(通常是主庫),保證在Spring提交事務以前肯定數據源。git

保證在Spring提交事務以前肯定數據源,這個簡單,利用AOP寫個切換數據源的切面,讓他的優先級高於Spring事務切面的優先級。github

至於讀,寫方法的區分能夠用2個註解。web

可是如何切換數據庫呢?我徹底不知道!多年經驗告訴我spring

當徹底不瞭解一個技術時,先搜索學習必要知識,以後再動手嘗試。sql

我搜索了一些網文,發現都提到了一個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的方式在多個數據庫中進行切換。編程

重點關注setTargetDataSources,setDefaultTargetDataSource,determineCurrentLookupKey三個方法。那麼AbstractRoutingDataSource就是Spring讀寫分離的關鍵了。安全

仔細閱讀了三個方法,基本上跟方法名的意思一致。setTargetDataSources設置備選的數據源集合。setDefaultTargetDataSource設置默認數據源,determineCurrentLookupKey決定當前數據源的對應的key。springboot

可是我很好奇這3個方法都沒有包含切換數據庫的邏輯啊!我仔細閱讀源碼發現一個方法,determineTargetDataSource方法,其實它纔是獲取數據源的實現。源碼以下:

//切換數據庫的核心邏輯
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個核心方法
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
  this.targetDataSources = targetDataSources;
}

public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
  this.defaultTargetDataSource = defaultTargetDataSource;
}

簡單說就是,根據determineCurrentLookupKey獲取的key,在resolvedDataSources這個Map中查找對應的datasource!,注意determineTargetDataSource方法居然不使用的targetDataSources!

那必定存在resolvedDataSources與targetDataSources的對應關係。我接着翻閱代碼,發現一個afterPropertiesSet方法(Spring源碼中InitializingBean接口中的方法),這個方法將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實例已經建立好,且屬性值和依賴的其餘bean實例都已經注入之後執行。

也就是說調用,targetDataSources,defaultTargetDataSource的賦值必定要在afterPropertiesSet前邊執行。

如今都是 Spring Boot 天下了,這個倉庫推給你們學習下:https://github.com/javastacks/spring-boot-best-practice

AbstractRoutingDataSource簡單總結:

  1. AbstractRoutingDataSource,內部有一個Map<Object,DataSource>的域resolvedDataSources

  2. determineTargetDataSource方法經過determineCurrentLookupKey方法得到key,進而從map中取得對應的DataSource。

  3. setTargetDataSources 設置 targetDataSources

  4. setDefaultTargetDataSource 設置 defaultTargetDataSource,

  5. targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉換爲resolvedDataSources和resolvedDefaultDataSource。

  6. targetDataSources,defaultTargetDataSource的賦值必定要在afterPropertiesSet前邊執行。

進一步瞭解理論後,讀寫分離的方式則基本上出如今眼前了。(「下列方法不惟一」)

先寫一個類繼承AbstractRoutingDataSource,實現determineCurrentLookupKey方法,和afterPropertiesSet方法。afterPropertiesSet方法中調用setDefaultTargetDataSource和setTargetDataSources方法以後調用super.afterPropertiesSet。

以後定義一個切面在事務切面以前執行,肯定真實數據源對應的key。Spring事務失效的 8 大緣由,這篇推薦看下。

可是這又出現了一個問題,如何線程安全的狀況下傳遞每一個線程獨立的key呢?沒錯使用ThreadLocal傳遞真實數據源對應的key。

ThreadLocal,Thread的局部變量,確保每個線程都維護變量的一個副本,詳細教程能夠關注公衆號Java技術棧搜索閱讀。

到這裏基本邏輯就想通了,以後就是寫了。

DataSourceContextHolder 使用ThreadLocal存儲真實數據源對應的key

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

DataSourceAopAspect 切面切換真實數據源對應的key,並設置優先級保證高於事務切面

@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() {  
        //若是已經開啓寫事務了,那以後的全部讀都從寫庫讀  
            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() {
  /** 
         * 值越小,越優先執行 要優於事務的執行 
         * 在啓動類中加上了@EnableTransactionManagement(order = 10)  
         */  
  return 1;
 }
}

RoutingDataSouceImpl實現AbstractRoutingDataSource的邏輯

@Component
public class RoutingDataSouceImpl extends AbstractRoutingDataSource {
 
 @Override
 public void afterPropertiesSet() {
  //初始化bean的時候執行,能夠針對某個具體的bean進行配置
  //afterPropertiesSet 早於init-method
  //將datasource注入到targetDataSources中,能夠爲後續路由用到的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);
  //執行原有afterPropertiesSet邏輯,
  //即將targetDataSources中的DataSource加載到resolvedDataSources
  super.afterPropertiesSet();
 }
 @Override
 protected Object determineCurrentLookupKey() {
  //這裏邊就是讀寫分離邏輯,最後返回的是setTargetDataSources保存的Map對應的key
  String typeKey = DataSourceContextHolder.getReadOrWrite();  
  Assert.notNull(typeKey, "數據庫路由發現typeKey is null,沒法抉擇使用哪一個庫");
  log.info("使用"+typeKey+"數據庫.............");  
  return typeKey;
 }
   private static Logger log = LoggerFactory.getLogger(RoutingDataSouceImpl.class); 
 @Autowired  
 @Qualifier("writeDataSource")  
 private DataSource writeDataSource;  
 @Autowired  
 @Qualifier("readDataSource")  
 private DataSource readDataSource;  
}

基本邏輯實現完畢了就進行,通用設置,設置數據源,事務,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();
    // 實體類對應的位置
    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事務管理器
  DataSourceTransactionManager transactionManager = new 
                DataSourceTransactionManager(roundRobinDataSouceProxy);
  return transactionManager;
}

其餘代碼,就不在這裏贅述了。如今都是 Spring Boot 天下了,這個倉庫推給你們學習下:https://github.com/javastacks/spring-boot-best-practice

使用Spring寫讀寫分離,其核心就是AbstractRoutingDataSource,源碼不難,讀懂以後,寫個讀寫分離就簡單了!。

AbstractRoutingDataSource重點回顧:

  1. AbstractRoutingDataSource,內部有一個Map<Object,DataSource>的域resolvedDataSources

  2. determineTargetDataSource方法經過determineCurrentLookupKey方法得到key,進而從map中取得對應的DataSource。

  3. setTargetDataSources 設置 targetDataSources

  4. setDefaultTargetDataSource 設置 defaultTargetDataSource,

  5. targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉換爲resolvedDataSources和resolvedDefaultDataSource。

  6. targetDataSources,defaultTargetDataSource的賦值必定要在afterPropertiesSet前邊執行。

這周確實有點忙,週五花費了些時間不過總算實現了本身的諾言。

完成承諾不容易,喜歡您就點個贊!

做者:溫安適
來源:https://my.oschina.net/floor/blog/1632565


END


我是武哥,最後給你們 免費分享我寫的 10 萬字 Spring Boot 學習筆記(帶完整目錄)以及對應的源碼 。這是我以前在 CSDN 開的一門課,因此筆記很是詳細完整,我準備將資料分享出來給你們免費學習,相信你們看完必定會有所收穫( 下面有下載方式 )。


能夠看出,我當時備課很是詳細,目錄很是完整,讀者能夠手把手跟着筆記,結合源代碼來學習。如今免費分享出來,有須要的讀者能夠下載學習,就在我公衆號回覆:筆記,就行。



若有文章對你有幫助,

在看轉發是對我最大的支持



關注Java開發寶典

天天學習技術乾貨



點贊是最大的支持 


本文分享自微信公衆號 - 武哥聊編程(eson_15)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索