Spring Aop實現資料庫讀寫分離

雨中漫步我心飛揚發表於2018-08-23

讀寫分離

資料庫讀寫分離是讓主資料庫處理事務性查詢,而從資料庫處理SELECT查詢,也就是主資料庫主要處理新增,修改,刪除類的操作,當然也可以處理查詢操作。通過增加物理機器,降低資料庫的寫壓力,主從職責明確,很大程度避免了X鎖和s鎖的爭用。主從分離,適用於可以接受一定程度的讀延遲,主從同步是通過主庫的binlog日誌來同步資料的,會有一定的資料延遲。

方案簡單對比分析

要實現讀寫分離就要考慮解決多資料來源的問題。
a.可以採用Spring Transaction對多資料來源支援的方案,在專案中配置多個資料來源,多個sqlsesssion,在使用時通過註解指定資料來源。

@Transactional("transactionManager1")

b.可以採用噹噹開源ShardingJDBC來實現多資料來源的配置。
c.可以採用aop攔截,設定不同的業務採用不同的資料來源。

對於方案a如果有的語句不需要在事務中執行,此時沒有了註解哪麼怎麼確定採用哪種資料來源呢?而且在事務巢狀中容易出現問題。方案b對事務支援不好,是弱事務的,也就是如果事務中間執行失敗了,程式碼不會回滾,只會以嘗試一定次數的方式來保證執行失敗的任務重新執行,這對於冪等性、資料一致性都存在一定程度的問題。本文僅探討aop的方式實現多資料來源,從而實現讀寫分離。

spring aop實現

aop主要是為了在執行資料庫操作之前攔截,然後設定資料來源,這時考慮到事務的特點,所以對service層進行攔截。建立切面類DataSourceAspect,程式碼如下:

    @Pointcut(".......")
    public void dataSourceAspectj() {
    }

    @Around(value = "dataSourceAspectj()")
    public Object aroundAdvice(final ProceedingJoinPoint point) throws Throwable {
        try {
            String dataSourceType = DataSourceTypeManager.getDataSourceType();
            if (符合某種條件) {
                DataSourceTypeManager.setDataSourceType(DataSourceTypeManager.DATA_SOURCE_MASTER);
            }else{
                DataSourceTypeManager.setDataSourceType(DataSourceTypeManager.DATA_SOURCE_SLAVER);
            }
            return point.proceed();
        } finally {
            DataSourceTypeManager.clearDataSourceType();
        }

    }
複製程式碼

DataSourceTypeManager類來儲存資料來源型別

public class DataSourceTypeManager {
    public static String DATA_SOURCE_SLAVE = "slave";
    public static String DATA_SOURCE_MASTER = "master";

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }
    public static String getDataSourceType() {
        return contextHolder.get();
    }
    public static void clearDataSourceType() {
        contextHolder.remove();
    }
}
複製程式碼

看了上面的程式碼你可能存在疑問,設定了資料來源型別,在哪裡會用到呢?在執行sql語句之前,我們都知道需要先獲取資料庫的連線,生成statement等一系列步驟才能得到想要的結果,那麼我們設定的資料來源型別就是在獲取資料庫連線的時候用到的。獲取資料的連線在AbstractRoutingDataSource類中,該類的原始碼部分如下(可以忽略不看):

	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}
	。。。。。
	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;
	}
複製程式碼

而上面的程式碼determineCurrentLookupKey是個抽象方法,我們只需實現該抽象方法返回自己的資料來源即可,因此定義DynamicDataSource類

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
     
        return DataSourceTypeManager.getDataSourceType();
    
    }
}
複製程式碼

DynamicDataSource繼承了AbstractRoutingDataSource, 也是DataSource類,資料來源配置的程式碼如下:

@Bean(name="dataSource")
    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slaveDataSource") DataSource slaveDataSource
                                        ) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceTypeManager.DATA_SOURCE_SLAVE, slaveDataSource);
        targetDataSources.put(DataSourceTypeManager.DATA_SOURCE_MASTER, masterDataSource);
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(masterDataSource);
        return dataSource;
    }
複製程式碼

注意事項

當在service層呼叫dao層進行資料庫處理時,若service沒有啟動事務機制,則執行的順序為:切面——>determineCurrentLookupKey——>Dao方法。而當在service層啟動事務時,由於在一個事務中執行失敗後會回滾之前所執行的所有操作,因此spring會在service方法執行前呼叫determineCurrentLookupKey,那麼需要在切面上設定如下註解,才能保證先執行切面。

@Order(1)

相關文章