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

Java技術棧
www.javastack.cn
關注閱讀更多優(yōu)質文章
寫了個讀寫分離,并未照搬網文,而是獨立思考后的成果,寫完以后發(fā)現從零開始寫讀寫分離并不難!
我最初的想法是:讀方法走讀庫,寫方法走寫庫(一般是主庫),保證在Spring提交事務之前確定數據源。

保證在Spring提交事務之前確定數據源,這個簡單,利用AOP寫個切換數據源的切面,讓他的優(yōu)先級高于Spring事務切面的優(yōu)先級。
至于讀,寫方法的區(qū)分可以用2個注解。
但是如何切換數據庫呢?我完全不知道!多年經驗告訴我
當完全不了解一個技術時,先搜索學習必要知識,之后再動手嘗試。
我搜索了一些網文,發(fā)現都提到了一個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。
但是我很好奇這3個方法都沒有包含切換數據庫的邏輯??!我仔細閱讀源碼發(fā)現一個方法,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簡單說就是,根據determineCurrentLookupKey獲取的key,在resolvedDataSources這個Map中查找對應的datasource!,注意determineTargetDataSource方法竟然不使用的targetDataSources!
那一定存在resolvedDataSources與targetDataSources的對應關系。我接著翻閱代碼,發(fā)現一個afterPropertiesSet方法(Spring源碼中InitializingBean接口中的方法),這個方法將targetDataSources的值賦予了resolvedDataSources。
源碼如下:
@Override
public?void?afterPropertiesSet()?{
??if?(this.targetDataSources?==?null)?{
???throw?new?IllegalArgumentException("Property?'targetDataSources'?is?required");
??}
??this.resolvedDataSources?=?new?HashMapafterPropertiesSet 方法,熟悉Spring的都知道,它在bean實例已經創(chuàng)建好,且屬性值和依賴的其他bean實例都已經注入以后執(zhí)行。
也就是說調用,targetDataSources,defaultTargetDataSource的賦值一定要在afterPropertiesSet前邊執(zhí)行。
現在都是 Spring Boot 天下了,這個倉庫推給大家學習下:https://github.com/javastacks/spring-boot-best-practice
AbstractRoutingDataSource簡單總結:
AbstractRoutingDataSource,內部有一個Map
的域resolvedDataSources determineTargetDataSource方法通過determineCurrentLookupKey方法獲得key,進而從map中取得對應的DataSource。
setTargetDataSources 設置 targetDataSources
setDefaultTargetDataSource 設置 defaultTargetDataSource,
targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉換為resolvedDataSources和resolvedDefaultDataSource。
targetDataSources,defaultTargetDataSource的賦值一定要在afterPropertiesSet前邊執(zhí)行。
進一步了解理論后,讀寫分離的方式則基本上出現在眼前了。(“下列方法不唯一”)
先寫一個類繼承AbstractRoutingDataSource,實現determineCurrentLookupKey方法,和afterPropertiesSet方法。afterPropertiesSet方法中調用setDefaultTargetDataSource和setTargetDataSources方法之后調用super.afterPropertiesSet。
之后定義一個切面在事務切面之前執(zhí)行,確定真實數據源對應的key。Spring事務失效的 8 大原因,這篇推薦看下。
但是這又出現了一個問題,如何線程安全的情況下傳遞每個線程獨立的key呢?沒錯使用ThreadLocal傳遞真實數據源對應的key。
ThreadLocal,Thread的局部變量,確保每一個線程都維護變量的一個副本,詳細教程可以關注公眾號Java技術棧搜索閱讀。
到這里基本邏輯就想通了,之后就是寫了。
DataSourceContextHolder 使用ThreadLocal存儲真實數據源對應的key
public?class?DataSourceContextHolder?{??
????private?static?Logger?log?=?LoggerFactory.getLogger(DataSourceContextHolder.class);?//線程本地環(huán)境??
????private?static?final?ThreadLocal?local?=?new?ThreadLocal();???
????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,并設置優(yōu)先級保證高于事務切面
@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()?{
??/**?
?????????*?值越小,越優(yōu)先執(zhí)行?要優(yōu)于事務的執(zhí)行?
?????????*?在啟動類中加上了@EnableTransactionManagement(order?=?10)??
?????????*/??
??return?1;
?}
}
RoutingDataSouceImpl實現AbstractRoutingDataSource的邏輯
@Component
public?class?RoutingDataSouceImpl?extends?AbstractRoutingDataSource?{
?
?@Override
?public?void?afterPropertiesSet()?{
??//初始化bean的時候執(zhí)行,可以針對某個具體的bean進行配置
??//afterPropertiesSet?早于init-method
??//將datasource注入到targetDataSources中,可以為后續(xù)路由用到的key
??this.setDefaultTargetDataSource(writeDataSource);
??MaptargetDataSources=new?HashMap();
??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對應的key
??String?typeKey?=?DataSourceContextHolder.getReadOrWrite();??
??Assert.notNull(typeKey,?"數據庫路由發(fā)現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
的域resolvedDataSources determineTargetDataSource方法通過determineCurrentLookupKey方法獲得key,進而從map中取得對應的DataSource。
setTargetDataSources 設置 targetDataSources
setDefaultTargetDataSource 設置 defaultTargetDataSource,
targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉換為resolvedDataSources和resolvedDefaultDataSource。
targetDataSources,defaultTargetDataSource的賦值一定要在afterPropertiesSet前邊執(zhí)行。
這周確實有點忙,周五花費了些時間不過總算實現了自己的諾言。
完成承諾不容易,喜歡您就點個贊!
作者:溫安適
來源:https://my.oschina.net/floor/blog/1632565






關注Java技術??锤喔韶?/strong>


