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

作者 | 溫安適
最近在學(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é):
AbstractRoutingDataSource,內(nèi)部有一個(gè)Map<Object,DataSource>的域resolvedDataSources determineTargetDataSource方法通過determineCurrentLookupKey方法獲得key,進(jìn)而從map中取得對(duì)應(yīng)的DataSource。 setTargetDataSources 設(shè)置 targetDataSources setDefaultTargetDataSource 設(shè)置 defaultTargetDataSource, targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉(zhuǎn)換為resolvedDataSources和resolvedDefaultDataSource。 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)回顧:
AbstractRoutingDataSource,內(nèi)部有一個(gè)Map<Object,DataSource>的域resolvedDataSources determineTargetDataSource方法通過determineCurrentLookupKey方法獲得key,進(jìn)而從map中取得對(duì)應(yīng)的DataSource。 setTargetDataSources 設(shè)置 targetDataSources setDefaultTargetDataSource 設(shè)置 defaultTargetDataSource, targetDataSources和defaultTargetDataSource 在afterPropertiesSet分別轉(zhuǎn)換為resolvedDataSources和resolvedDefaultDataSource。 targetDataSources,defaultTargetDataSource的賦值一定要在afterPropertiesSet前邊執(zhí)行。
這周確實(shí)有點(diǎn)忙,周五花費(fèi)了些時(shí)間不過總算實(shí)現(xiàn)了自己的諾言。
完成承諾不容易,喜歡您就點(diǎn)個(gè)贊!

