Read database springboot + mybatis achieve separation

Introduction

With the development of business, in addition to split the business modules, database read and write separation is a common optimization methods.
Use the program AbstractRoutingDataSourceand mybatis pluginto dynamically select a data source
reason for choosing this program does not require major changes to the existing business code, very friendly

注:
demo中使用了mybatis-plus,实际使用mybatis也是一样的
demo中使用的数据库是postgres,实际任一类型主从备份的数据库示例都是一样的
demo中使用了alibaba的druid数据源,实际其他类型的数据源也是一样的
复制代码

surroundings

First, we need two database instances, one for the master, as a slave.
All write operations, we operate on the master node
all read operations, we operate on the slave node

需要注意的是:对于一次有读有写的事务,事务内的读操作也不应该在slave节点上,所有操作都应该在master节点上
复制代码

Examples of start and up to two pg, wherein the node corresponding master port 15432, 15433 slave node corresponding to the port:

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
复制代码

achieve

The entire implementation There are three main parts:

  • Configure two data sources
  • Achieved AbstractRoutingDataSourceby the use of dynamic data source
  • Implemented mybatis pluginto dynamically select a data source

Configuration data source

The connection information database configuration file to 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 the write data source corresponding to master node port 15432
read read data source, the corresponding slave node to port 15433

The injection of two data source information 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();
    }
}
复制代码

Achieve AbstractRoutingDataSource

Providing spring AbstractRoutingDataSource, a dynamic data source selection function, to replace the original single data source, the separate read and write can be realized:

@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;
    }

}

复制代码

AbstractRoutingDataSourceAn internal maintenance Map<Object, Object>of the Map
in the initialization process, we will write, read two data sources are added to the map
when calling the data source: determineCurrentLookupKey () method returns the corresponding data source you want to use key

The current thread need to use the data source corresponding key, in DataSourceHolderthe maintenance class:

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();
    }

}
复制代码

Achieve mybatis plugin

The above-mentioned source data corresponding to the current thread using key, the key needs to mybatis pluginbe determined according to the type of sql MybatisDataSourceInterceptorcategories:

@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) {
    }
}
复制代码

Only if the transaction is not, and select the type of call is sql, in the data source to read DataSourceHolder
other cases, AbstractRoutingDataSourceuses the default write data source

So far, the project has been automatically at reading and writing data between source switching, without modifying existing business code
Finally, a demo version Dependency

<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>
复制代码

Guess you like

Origin juejin.im/post/5d6f1e0e6fb9a06af92bc0c4