讀寫分離
資料庫讀寫分離是讓主資料庫處理事務性查詢,而從資料庫處理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)