javascript
基于Spring读写分离
為什么是基于Spring的呢,因為實現(xiàn)方案基于Spring的事務(wù)以及AbstractRoutingDataSource(spring中的一個基礎(chǔ)類,可以在其中放多個數(shù)據(jù)源,然后根據(jù)一些規(guī)則來確定當(dāng)前需要使用哪個數(shù)據(jù),既可以進行讀寫分離,也可以用來做分庫分表)
我們只需要實現(xiàn)
determineCurrentLookupKey()每次生成jdbc connection時,都會先調(diào)用該方法來甄選出實際需要使用的datasource,由于這個方法并沒有參數(shù),因此,最佳的方式是基于ThreadLocal變量
@Component @Primary public class DynamicRoutingDataSource extends AbstractRoutingDataSource {@Resource(name = "masterDs")private DataSource masterDs;@Resource(name = "slaveDs")private DataSource slaveDs;@Overridepublic void afterPropertiesSet() {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("slaveDs", slaveDs);targetDataSources.put("masterDs", masterDs);this.setTargetDataSources(targetDataSources);this.setDefaultTargetDataSource(masterDs);super.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceHolder.contextHolder.get();}/*** 持有當(dāng)前線程所有數(shù)據(jù)源的程序*/public static class DynamicDataSourceHolder {public static final ThreadLocal<String> contextHolder = new ThreadLocal<>();public static void setWrite() {contextHolder.set("masterDs");}public static void setRead() {contextHolder.set("slaveDs");}public static void remove() {contextHolder.remove();}} }DynamicRoutingDataSource 僅僅是一個DataSource實現(xiàn),更高層的類主要用它來創(chuàng)建連接,如getConnection(),getConnection會先尋找實際的datasource,在創(chuàng)建連接
@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}以上代碼,使用了ThreadLocal contextHolder來存取當(dāng)前需要的datasource的loopkey
contextHolder上的值可能沒有被初始化,此時contextHolder.get() 等于 null,此時會使用默認(rèn)的datasource,我們設(shè)置的默認(rèn)值對應(yīng)的是主庫
下一步,是在sql請求前,先對contextHolder賦值,手動賦值工作量很大,并且容易出錯,也沒有辦法規(guī)范
實現(xiàn)方案是基于Aop,根據(jù)方法名,或者方法注解來區(qū)分
- 根據(jù)方法名的好處比較明顯,只要工程的manager\dao層方法名稱規(guī)范就行,不用到處添加注解,缺點是不精準(zhǔn)
- 根據(jù)注解來區(qū)分,缺點是需要在很多個類或接口上加注解,優(yōu)點是精準(zhǔn)
如果有特殊的業(yè)務(wù),可以兩種情況都使用,如以下場景:
- 一個大的service,先后調(diào)用多個manager層\dao層sql,先insert、后query,并且想直接查主庫,也就是寫后立即查,由于mysql主從同步可能會有延遲,寫后立即查可能會讀到老數(shù)據(jù),寫后立即查的情況比較復(fù)雜,如果不是事務(wù)的話,實現(xiàn)其實比較復(fù)雜,如何在非事務(wù)場景下,讓兩個順序執(zhí)行的請求,保持同一個connection,需單獨調(diào)研,根據(jù)實際情況進行修改
我們將設(shè)置contextHolder的地方加在了dao層,出于以下考量:
- 透過manager層,直接調(diào)用dao時,無風(fēng)險
- 當(dāng)前工程采用的是手動在manager層開啟事務(wù),開啟事務(wù)lookupKey一定為null,采用默認(rèn)的masterDs,沒有問題,事務(wù)開啟鏈接后,dao層的每個被調(diào)用的方法,會使用事務(wù)中的鏈接(由Spring Transaction控制)
- 如果在manager層開啟事務(wù),manager層的方法名可能不規(guī)范,dao層是最接近sql請求的地方,規(guī)范更容易遵循
當(dāng)然,這不是最佳實踐,如果在manager層做這個事,也是可以的,看具體的情況,要求是,名稱或注解表意為query的方法,里面不能做任何更新操作,因為manager層已經(jīng)確定了它會查從庫,以下方法是會執(zhí)行失敗的
public class Manager1{......public List getSome(params){dao1.getRecord1(params);dao2.updateLog(params);}}因為dao2是一個更新請求,使用從庫進行更新,肯定是會失敗的
(使用Spring Boot,或Spring時,需確保開啟AspectJ,Spring Boot開啟方式 @EnableAspectJAutoProxy(proxyTargetClass = true)
)
aop示例
@Aspect @Order(-10) @Component public class DataSourceAspect {public static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);private static final String[] DefaultSlaveMethodStart= new String[]{"query", "find", "get", "select", "count", "list"};/*** 切入點,所有的mapper pcakge下面的類的方法*/@Pointcut(value = "execution(* com.xx.xx.dao.mapper..*.*(..))")@SuppressWarnings("all")public void pointCutTransaction() {}/*** 根據(jù)方法名稱,判斷是讀還是寫* @param jp*/@Before("pointCutTransaction()")public void doBefore(JoinPoint jp) {String methodName = jp.getSignature().getName();if (isReadReq(methodName)) {DynamicRoutingDataSource.DynamicDataSourceHolder.setRead();} else {DynamicRoutingDataSource.DynamicDataSourceHolder.setWrite();}}/*** 方法結(jié)束 finally 時執(zhí)行* @param jp*/@After("pointCutTransaction()")public void after(JoinPoint jp) {DynamicRoutingDataSource.DynamicDataSourceHolder.remove();}/*** 根據(jù)方法名,判斷是否為讀請求** @param methodName* @return*/private boolean isReadReq(String methodName) {for (String start : DefaultSlaveMethodStart) {if (methodName.startsWith(start)) {return true;}}return false;} }上面的代碼,根據(jù)方法名稱前綴,反向判斷哪些方法應(yīng)該使用從庫,凡是不匹配的方法,都走主庫
方法結(jié)束后,必須清空contextHolder,否則他可能發(fā)生混亂,如這里是manager層 call dao層,dao層退出執(zhí)行后,不清空contextHolder,則manager層開啟事務(wù)時,會直接使用dao的值,如果這個請求是query,分配給從庫了,那么manager層開啟事務(wù)時就用的是從庫了,結(jié)果可想而知
如此就完成了讀寫分離
spring 事務(wù)
當(dāng)前使用的是編程聲明式事務(wù)
@Overridepublic <T> T execute(TransactionCallback<T> action) throws TransactionException {if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);}else {TransactionStatus status = this.transactionManager.getTransaction(this);T result;try {result = action.doInTransaction(status);}catch (RuntimeException ex) {// Transactional code threw application exception -> rollbackrollbackOnException(status, ex);throw ex;}catch (Error err) {// Transactional code threw error -> rollbackrollbackOnException(status, err);throw err;}catch (Throwable ex) {// Transactional code threw unexpected exception -> rollbackrollbackOnException(status, ex);throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");}this.transactionManager.commit(status);return result;}}來發(fā)起事務(wù),execute方法中
this.transactionManager.getTransaction(this)將獲取事務(wù)需要的鏈接,其內(nèi)部會讀取ThreadLocal變量,判斷是否有事務(wù)連接,如果已有,或允許嵌套事務(wù),則會重復(fù)利用當(dāng)前事務(wù)鏈接
如果當(dāng)前請求已經(jīng)被包含到了事務(wù)中,則根據(jù)策略,判斷是否新開一個事務(wù)或不使用事務(wù),它們的方法基本相同:通過創(chuàng)建一個新的連接來處理,并將當(dāng)前已被打開的事務(wù)先掛起,等待當(dāng)前操作執(zhí)行結(jié)束后,再恢復(fù)外部事務(wù),繼續(xù)執(zhí)行
TransactionSynchronizationManager類記錄了這些ThreadLocal變量,允許將事務(wù)bind到其resources中,DatasourceTransactionManager則會觸發(fā)這些操作
轉(zhuǎn)載于:https://www.cnblogs.com/windliu/p/8920467.html
總結(jié)
以上是生活随笔為你收集整理的基于Spring读写分离的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android中onInterceptT
- 下一篇: 使用JS实现2048小游戏