javascript
基于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,我們設置的默認值對應的是主庫
下一步,是在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則會觸發這些操作
轉載于:https://www.cnblogs.com/windliu/p/8920467.html
總結
以上是生活随笔為你收集整理的基于Spring读写分离的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android中onInterceptT
- 下一篇: 20155216 Exp5 MSF基础应