寫了個讀寫分離,並未照搬網文,而是獨立思考後的成果,寫完之後發現從零開始寫讀寫分離並不難!
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簡單總結:
AbstractRoutingDataSource,內部有一個Map<Object,DataSource>的域resolvedDataSources
determineTargetDataSource方法經過determineCurrentLookupKey方法得到key,進而從map中取得對應的DataSource。
setTargetDataSources 設置 targetDataSources
setDefaultTargetDataSource 設置 defaultTargetDataSource,
targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉換爲resolvedDataSources和resolvedDefaultDataSource。
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重點回顧:
AbstractRoutingDataSource,內部有一個Map<Object,DataSource>的域resolvedDataSources
determineTargetDataSource方法經過determineCurrentLookupKey方法得到key,進而從map中取得對應的DataSource。
setTargetDataSources 設置 targetDataSources
setDefaultTargetDataSource 設置 defaultTargetDataSource,
targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉換爲resolvedDataSources和resolvedDefaultDataSource。
targetDataSources,defaultTargetDataSource的賦值必定要在afterPropertiesSet前邊執行。
這周確實有點忙,週五花費了些時間不過總算實現了本身的諾言。
完成承諾不容易,喜歡您就點個贊!
做者:溫安適
來源:https://my.oschina.net/floor/blog/1632565
END
我是武哥,最後給你們 免費分享我寫的 10 萬字 Spring Boot 學習筆記(帶完整目錄)以及對應的源碼 。這是我以前在 CSDN 開的一門課,因此筆記很是詳細完整,我準備將資料分享出來給你們免費學習,相信你們看完必定會有所收穫( 下面有下載方式 )。
能夠看出,我當時備課很是詳細,目錄很是完整,讀者能夠手把手跟着筆記,結合源代碼來學習。如今免費分享出來,有須要的讀者能夠下載學習,就在我公衆號回覆:筆記,就行。
若有文章對你有幫助,
在看和轉發是對我最大的支持!
關注Java開發寶典
天天學習技術乾貨
點贊是最大的支持
本文分享自微信公衆號 - 武哥聊編程(eson_15)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。