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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

基于Spring读写分离

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

為什么是基于Spring的呢,因為實現方案基于Spring的事務以及AbstractRoutingDataSource(spring中的一個基礎類,可以在其中放多個數據源,然后根據一些規則來確定當前需要使用哪個數據,既可以進行讀寫分離,也可以用來做分庫分表)

我們只需要實現

determineCurrentLookupKey()

每次生成jdbc connection時,都會先調用該方法來甄選出實際需要使用的datasource,由于這個方法并沒有參數,因此,最佳的方式是基于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();}/*** 持有當前線程所有數據源的程序*/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實現,更高層的類主要用它來創建連接,如getConnection(),getConnection會先尋找實際的datasource,在創建連接

@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}

以上代碼,使用了ThreadLocal contextHolder來存取當前需要的datasource的loopkey
contextHolder上的值可能沒有被初始化,此時contextHolder.get() 等于 null,此時會使用默認的datasource,我們設置的默認值對應的是主庫

/*** 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賦值,手動賦值工作量很大,并且容易出錯,也沒有辦法規范

實現方案是基于Aop,根據方法名,或者方法注解來區分

  • 根據方法名的好處比較明顯,只要工程的manager\dao層方法名稱規范就行,不用到處添加注解,缺點是不精準
  • 根據注解來區分,缺點是需要在很多個類或接口上加注解,優點是精準

如果有特殊的業務,可以兩種情況都使用,如以下場景:

  • 一個大的service,先后調用多個manager層\dao層sql,先insert、后query,并且想直接查主庫,也就是寫后立即查,由于mysql主從同步可能會有延遲,寫后立即查可能會讀到老數據,寫后立即查的情況比較復雜,如果不是事務的話,實現其實比較復雜,如何在非事務場景下,讓兩個順序執行的請求,保持同一個connection,需單獨調研,根據實際情況進行修改

我們將設置contextHolder的地方加在了dao層,出于以下考量:

  • 透過manager層,直接調用dao時,無風險
  • 當前工程采用的是手動在manager層開啟事務,開啟事務lookupKey一定為null,采用默認的masterDs,沒有問題,事務開啟鏈接后,dao層的每個被調用的方法,會使用事務中的鏈接(由Spring Transaction控制)
  • 如果在manager層開啟事務,manager層的方法名可能不規范,dao層是最接近sql請求的地方,規范更容易遵循

當然,這不是最佳實踐,如果在manager層做這個事,也是可以的,看具體的情況,要求是,名稱或注解表意為query的方法,里面不能做任何更新操作,因為manager層已經確定了它會查從庫,以下方法是會執行失敗的

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() {}/*** 根據方法名稱,判斷是讀還是寫* @param jp*/@Before("pointCutTransaction()")public void doBefore(JoinPoint jp) {String methodName = jp.getSignature().getName();if (isReadReq(methodName)) {DynamicRoutingDataSource.DynamicDataSourceHolder.setRead();} else {DynamicRoutingDataSource.DynamicDataSourceHolder.setWrite();}}/*** 方法結束 finally 時執行* @param jp*/@After("pointCutTransaction()")public void after(JoinPoint jp) {DynamicRoutingDataSource.DynamicDataSourceHolder.remove();}/*** 根據方法名,判斷是否為讀請求** @param methodName* @return*/private boolean isReadReq(String methodName) {for (String start : DefaultSlaveMethodStart) {if (methodName.startsWith(start)) {return true;}}return false;} }

上面的代碼,根據方法名稱前綴,反向判斷哪些方法應該使用從庫,凡是不匹配的方法,都走主庫
方法結束后,必須清空contextHolder,否則他可能發生混亂,如這里是manager層 call dao層,dao層退出執行后,不清空contextHolder,則manager層開啟事務時,會直接使用dao的值,如果這個請求是query,分配給從庫了,那么manager層開啟事務時就用的是從庫了,結果可想而知

如此就完成了讀寫分離

spring 事務

當前使用的是編程聲明式事務

@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;}}

來發起事務,execute方法中

this.transactionManager.getTransaction(this)

將獲取事務需要的鏈接,其內部會讀取ThreadLocal變量,判斷是否有事務連接,如果已有,或允許嵌套事務,則會重復利用當前事務鏈接
如果當前請求已經被包含到了事務中,則根據策略,判斷是否新開一個事務或不使用事務,它們的方法基本相同:通過創建一個新的連接來處理,并將當前已被打開的事務先掛起,等待當前操作執行結束后,再恢復外部事務,繼續執行
TransactionSynchronizationManager類記錄了這些ThreadLocal變量,允許將事務bind到其resources中,DatasourceTransactionManager則會觸發這些操作

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

轉載于:https://www.cnblogs.com/windliu/p/8920467.html

總結

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

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。