介绍
随着业务的发展,除了拆分业务模块外,数据库的读写分离也是常见的优化手段。
方案使用了AbstractRoutingDataSource和mybatis plugin来动态的选择数据源
选择这个方案的原因主要是不需要改动原有业务代码,非常友好
注:demo中使用了mybatis-plus,实际使用mybatis也是一样的
demo中使用的数据库是postgres,实际任一类型主从备份的数据库示例都是一样的
demo中使用了alibaba的druid数据源,实际其他类型的数据源也是一样的
环境
首先,我们需要两个数据库实例,一为master,一为slave。
所有的写操作,我们在master节点上操作
所有的读操作,我们在slave节点上操作
需要注意的是:对于一次有读有写的事务,事务内的读操作也不应该在slave节点上,所有操作都应该在master节点上
先跑起来两个pg的实例,其中15432端口对应的master节点,15433端口对应的slave节点: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个部分:配置两个数据源
实现AbstractRoutingDataSource来动态的使用数据源
实现mybatis plugin来动态的选择数据源
配置数据源
将数据库连接信息配置到application.yml文件中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端口
将两个数据源信息注入为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();
}
}
实现AbstractRoutingDataSource
spring提供了AbstractRoutingDataSource,提供了动态选择数据源的功能,替换原有的单一数据源后,即可实现读写分离:@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 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的Map
在初始化过程中,我们将write、read两个数据源加入到这个map
调用数据源时:determineCurrentLookupKey()方法返回了需要使用的数据源对应的key
当前线程需要使用的数据源对应的key,是在DataSourceHolder类中维护的:public class DataSourceHolder {
public static final String WRITE_DATASOURCE = "write";
public static final String READ_DATASOURCE = "read";
private static final ThreadLocal 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();
}
}
实现mybatis plugin
上面提到了当前线程使用的数据源对应的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)) {
DataSourceHolder.putDataSource(DataSourceHolder.READ_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使用依赖版本
org.springframework.boot
spring-boot-starter-parent
2.1.7.RELEASE
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.postgresql
postgresql
42.2.2
com.alibaba
druid-spring-boot-starter
1.1.9
com.baomidou
mybatisplus-spring-boot-starter
1.0.5
com.baomidou
mybatis-plus
2.1.9
io.springfox
springfox-swagger2
2.8.0
io.springfox
springfox-swagger-ui
2.8.0
org.projectlombok
lombok
1.16.20
org.springframework.boot
spring-boot-starter-test
test