數據庫讀寫分離
數據庫讀寫分離環境搭建:https://github.com/niezhiliang/mysql-master-slave-docker
數據庫讀寫分離一般分爲兩種,一種是靜態的,一種是動態的,顧名思義靜態是配置了不會變,有侷限性,而動態的會根據業務的需求自動切換數據源
靜態
這種方式一般適用於項目需要依賴兩個不同的數據庫,而不是所謂的讀寫分離的主從數據庫。比如一個數據庫沒有用戶表,他想用自己另一個項目中的
用戶,這個時候用這種方式就比較合適啦。要想用這種方式來配置數據庫的讀寫分離也不是不行,那就把讀寫業務拆分出來嘛。 生成兩套xml和mapper.java, 一套用來處理
寫操作,另一套用來進行讀操作。功能能實現,但在寫代碼的時候就特別煩,有時候讀的業務寫到寫的數據庫就尷尬啦。
這種方式比較簡單,就不做說明啦,相信我自己也不會忘的。?
動態
這種方式就比較適用於數據庫的讀寫分離啦。這種方式解決了靜態方式的缺陷,共用一套mapper文件,無需拆分讀寫業務,用到aop對mapper的方法進行分析,判斷是讀操作還是寫操作,
並切換到相應的數據源。架構搭建好以後,編碼的時候無需關心什麼讀跟寫,只需專注於業務實現就可以啦。
-
配置好所有的讀寫數據源
@Configuration public class DataBaseConfiguration { @Value("${spring.datasource.type}") private Class<? extends DataSource> dataSourceType; //這個就是我們的寫數據源,也就是master庫 @Bean(name="writeDataSource", destroyMethod = "close", initMethod="init") @Primary @ConfigurationProperties(prefix = "spring.write.datasource") public DataSource writeDataSource() { return DataSourceBuilder.create() .type(dataSourceType).build(); } //配置第一個讀庫 所有讀數據源都需要將其配置成bean 並添加到讀數據源集合中 @Bean(name = "readDataSourceOne") @ConfigurationProperties(prefix = "spring.read.one.datasource") public DataSource readDataSourceOne() { return DataSourceBuilder.create() .type(dataSourceType).build(); } //配置第二個讀庫 @Bean(name = "readDataSourceTwo") @ConfigurationProperties(prefix = "spring.read.two.datasource") public DataSource readDataSourceTwo() { return DataSourceBuilder.create() .type(dataSourceType).build(); } //讀寫庫集合的Bean @Bean("readDataSources") public List<DataSource> readDataSources() { List<DataSource> dataSources = new ArrayList<DataSource>(); dataSources.add(readDataSourceOne()); dataSources.add(readDataSourceTwo()); return dataSources; } }
-
配置動態的sqlSessionFactory
@Configuration
@ConditionalOnClass({EnableTransactionManagement.class})
@MapperScan(basePackages = {"com.niezhiliang.db.read.write.separate.dynamic.type.mapper"})
public class TxxsbatisConfiguration {
//我們這裏使用的是alibaba druid連接池
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
//讀取配置的讀數據庫的數量(從庫)
@Value("${spring.datasource.readSize}")
private String dataSourceSize;
//我們的寫數據庫,也就是主數據庫
@Resource(name = "writeDataSource")
private DataSource dataSource;
//所有的讀數據源集合
@Resource(name = "readDataSources")
private List<DataSource> readDataSources;
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(roundRobinDataSourceProxy());
sqlSessionFactoryBean.setTypeAliasesPackage("com.niezhiliang.db.read.write.separate.dynamic.type.domain");
//mapper.xml文件位置
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mappers*//*.xml"));
sqlSessionFactoryBean.getObject().getConfiguration()
.setMapUnderscoreToCamelCase(true);
return sqlSessionFactoryBean.getObject();
}
//配置動態數據源的路由對象
@Bean
public AbstractRoutingDataSource roundRobinDataSourceProxy() {
TxxsAbstractRoutingDataSource proxy = new TxxsAbstractRoutingDataSource(readDataSources.size());
Map<Object,Object> targetDataSourceSize = new HashMap<Object,Object>();
targetDataSourceSize.put(dataSourceType.getTypeName(),dataSource);
for (int i = 0; i < readDataSources.size(); i++) {
targetDataSourceSize.put(i,readDataSources.get(i));
}
//如果沒有讀數據集 則默認爲寫數據庫
proxy.setDefaultTargetDataSource(dataSource);
//配置讀數據源
proxy.setTargetDataSources(targetDataSourceSize);
return proxy;
}
}
- 配置一個線程池,用於切換數據源(通過改變當前線程變量的方法,達到動態改變數據源)
public class DataSourceContextHolder {
private static final ThreadLocal<String> local = new ThreadLocal<String>();
public static ThreadLocal<String> getLocal() {
return local;
}
/**
* 讀可能是多個庫
*/
public static void read() {
local.set(DataSourceType.read.getType());
}
/**
* 寫只有一個庫
*/
public static void write() {
local.set(DataSourceType.write.getType());
}
public static String getJdbcType() {
return local.get();
}
}
- aop攔截所有的數據庫請求
@Aspect
@Component
public class DataSourceAop {
public static final Logger logger = LoggerFactory.getLogger(DataSourceAop.class);
//攔截mapper所有的讀操作,將數據源動態切換到讀數據源
@Before("execution(* com.niezhiliang.db.read.write.separate.dynamic.type.mapper..*.select*(..)) or execution(* com.niezhiliang.db.read.write.separate.dynamic.type.mapper..*..count*(..))")
public void setReadDataSourceType() {
DataSourceContextHolder.read();
logger.info("dataSource切換到:Read");
}
//攔截mapper所有的寫操作,將數據源動態切換到寫數據源
@Before(("execution(* com.niezhiliang.db.read.write.separate.dynamic.type.mapper..*.insert*(..)) or execution(* com.niezhiliang.db.read.write.separate.dynamic.type.mapper..*.update*(..)) or execution(* com.niezhiliang.db.read.write.separate.dynamic.type.mapper..*.delete*(..))"))
public void setWriteDataSourceType() {
DataSourceContextHolder.write();
logger.info("dataSource切換到:Write");
}
}
- 動態數據源切換
public class TxxsAbstractRoutingDataSource extends AbstractRoutingDataSource {
private int dataSourceNumber;
private AtomicInteger count = new AtomicInteger(0);
public TxxsAbstractRoutingDataSource(int dataSourceNumber) {
this.dataSourceNumber = dataSourceNumber;
}
@Override
protected Object determineCurrentLookupKey() {
//獲取當前線程是進行讀還是寫
String typeKey = DataSourceContextHolder.getJdbcType();
if (typeKey.equals(DataSourceType.write.getType()))
return DataSourceType.write.getType();
// 讀 簡單負載均衡 count會一直累加,那後模讀數據源的數量 達到簡單的負載均衡
int number = count.getAndAdd(1);
int lookupKey = number % dataSourceNumber;
return new Integer(lookupKey);
}
}