隨着業務的發展,除了拆分業務模塊外,數據庫的讀寫分離也是常見的優化手段。
方案使用了AbstractRoutingDataSource
和mybatis plugin
來動態的選擇數據源
選擇這個方案的緣由主要是不須要改動原有業務代碼,很是友好java
注:
demo中使用了mybatis-plus,實際使用mybatis也是同樣的
demo中使用的數據庫是postgres,實際任一類型主從備份的數據庫示例都是同樣的
demo中使用了alibaba的druid數據源,實際其餘類型的數據源也是同樣的
複製代碼
首先,咱們須要兩個數據庫實例,一爲master,一爲slave。
全部的寫操做,咱們在master節點上操做
全部的讀操做,咱們在slave節點上操做web
須要注意的是:對於一次有讀有寫的事務,事務內的讀操做也不該該在slave節點上,全部操做都應該在master節點上
複製代碼
先跑起來兩個pg的實例,其中15432端口對應的master節點,15433端口對應的slave節點:spring
docker run \
--name pg-master \
-p 15432:5432 \
--env 'PG_PASSWORD=postgres' \
--env 'REPLICATION_MODE=master' \
--env 'REPLICATION_USER=repluser' \
--env 'REPLICATION_PASS=repluserpass' \
-d sameersbn/postgresql:10-2
docker run \
--name pg-slave \
-p 15433:5432 \
--link pg-master:master \
--env 'PG_PASSWORD=postgres' \
--env 'REPLICATION_MODE=slave' \
--env 'REPLICATION_SSLMODE=prefer' \
--env 'REPLICATION_HOST=master' \
--env 'REPLICATION_PORT=5432' \
--env 'REPLICATION_USER=repluser' \
--env 'REPLICATION_PASS=repluserpass' \
-d sameersbn/postgresql:10-2
複製代碼
整個實現主要有3個部分:sql
AbstractRoutingDataSource
來動態的使用數據源mybatis plugin
來動態的選擇數據源將數據庫鏈接信息配置到application.yml文件中docker
spring:
mvc:
servlet:
path: /api
datasource:
write:
driver-class-name: org.postgresql.Driver
url: "${DB_URL_WRITE:jdbc:postgresql://localhost:15432/postgres}"
username: "${DB_USERNAME_WRITE:postgres}"
password: "${DB_PASSWORD_WRITE:postgres}"
read:
driver-class-name: org.postgresql.Driver
url: "${DB_URL_READ:jdbc:postgresql://localhost:15433/postgres}"
username: "${DB_USERNAME_READ:postgres}"
password: "${DB_PASSWORD_READ:postgres}"
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
複製代碼
write寫數據源,對應到master節點的15432端口
read讀數據源,對應到slave節點的15433端口shell
將兩個數據源信息注入爲DataSourceProperties
:數據庫
@Configuration
public class DataSourcePropertiesConfig {
@Primary
@Bean("writeDataSourceProperties")
@ConfigurationProperties("datasource.write")
public DataSourceProperties writeDataSourceProperties() {
return new DataSourceProperties();
}
@Bean("readDataSourceProperties")
@ConfigurationProperties("datasource.read")
public DataSourceProperties readDataSourceProperties() {
return new DataSourceProperties();
}
}
複製代碼
spring提供了AbstractRoutingDataSource
,提供了動態選擇數據源的功能,替換原有的單一數據源後,便可實現讀寫分離:api
@Component
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
@Resource(name = "writeDataSourceProperties")
private DataSourceProperties writeProperties;
@Resource(name = "readDataSourceProperties")
private DataSourceProperties readProperties;
@Override
public void afterPropertiesSet() {
DataSource writeDataSource =
writeProperties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
DataSource readDataSource =
readProperties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
setDefaultTargetDataSource(writeDataSource);
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(WRITE_DATASOURCE, writeDataSource);
dataSourceMap.put(READ_DATASOURCE, readDataSource);
setTargetDataSources(dataSourceMap);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
String key = DataSourceHolder.getDataSource();
if (key == null) {
// default datasource
return WRITE_DATASOURCE;
}
return key;
}
}
複製代碼
AbstractRoutingDataSource
內部維護了一個Map<Object, Object>
的Map
在初始化過程當中,咱們將write、read兩個數據源加入到這個map
調用數據源時:determineCurrentLookupKey()方法返回了須要使用的數據源對應的keymybatis
當前線程須要使用的數據源對應的key,是在DataSourceHolder
類中維護的:mvc
public class DataSourceHolder {
public static final String WRITE_DATASOURCE = "write";
public static final String READ_DATASOURCE = "read";
private static final ThreadLocal<String> local = new ThreadLocal<>();
public static void putDataSource(String dataSource) {
local.set(dataSource);
}
public static String getDataSource() {
return local.get();
}
public static void clearDataSource() {
local.remove();
}
}
複製代碼
上面提到了當前線程使用的數據源對應的key,這個key須要在mybatis plugin
根據sql類型來肯定 MybatisDataSourceInterceptor
類:
@Component
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
CacheKey.class, BoundSql.class})})
public class MybatisDataSourceInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if(!synchronizationActive) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
if(!ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
DataSourceHolder.putDataSource(DataSourceHolder.READ_DATASOURCE);
return invocation.proceed();
}
}
}
DataSourceHolder.putDataSource(DataSourceHolder.WRITE_DATASOURCE);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
複製代碼
僅當未在事務中,而且調用的sql是select類型時,在DataSourceHolder中將數據源設爲read
其餘狀況下,AbstractRoutingDataSource
會使用默認的write數據源
至此,項目已經能夠自動的在讀、寫數據源間切換,無需修改原有的業務代碼
最後,提供demo使用依賴版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatisplus-spring-boot-starter</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.1.9</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
複製代碼