日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

基于Spring读写分离

發(fā)布時間:2024/4/17 javascript 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于Spring读写分离 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

為什么是基于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)的是主庫

/*** Retrieve the current target DataSource. Determines the* {@link #determineCurrentLookupKey() current lookup key}, performs* a lookup in the {@link #setTargetDataSources targetDataSources} map,* falls back to the specified* {@link #setDefaultTargetDataSource default target DataSource} if necessary.* @see #determineCurrentLookupKey()*/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;}

下一步,是在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ā)這些操作

@Overrideprotected Object doSuspend(Object transaction) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;txObject.setConnectionHolder(null);return TransactionSynchronizationManager.unbindResource(this.dataSource); //掛起事務(wù)的基本原理:將外部事務(wù)放到新建事務(wù)(可能是非事務(wù))的suspendedResources上來進行掛起}@Overrideprotected void doResume(Object transaction, Object suspendedResources) {TransactionSynchronizationManager.bindResource(this.dataSource, suspendedResources); //將suspendedResources重新綁定到threadLocal變量}

轉(zhuǎn)載于:https://www.cnblogs.com/windliu/p/8920467.html

總結(jié)

以上是生活随笔為你收集整理的基于Spring读写分离的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。