Springboot+mybatis реализует разделение операций чтения и записи базы данных.

Java

вводить

С развитием бизнеса, в дополнение к разделению бизнес-модулей, разделение чтения и записи базы данных также является распространенным методом оптимизации.
Программа используетAbstractRoutingDataSourceиmybatis pluginдинамически выбирать источник данных
Основная причина выбора этого решения заключается в том, что нет необходимости изменять исходный бизнес-код, что очень удобно.

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

окрестности

Во-первых, нам нужны два экземпляра базы данных, один для главного и один для подчиненного.
Все операции записи мы выполняем на мастер-ноде
Все операции чтения мы оперируем на ведомом узле

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

Сначала запустите два экземпляра pg, главный узел, соответствующий порту 15432, и подчиненный узел, соответствующий порту 15433:

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

запись источника данных записи, соответствующего порту 15432 главного узла
read читает источник данных, соответствующий порту 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

весна обеспечивает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<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>карта
В процессе инициализации мы добавляем к этой карте два источника данных, запись и чтение.
При вызове источника данных: метод defineCurrentLookupKey() возвращает ключ, соответствующий используемому источнику данных.

Ключ, соответствующий источнику данных, который должен использовать текущий поток, находится вDataSourceHolderПоддерживается в классе:

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

}

Реализовать плагин mybatis

Выше указан ключ, соответствующий источнику данных, используемому текущим потоком.mybatis pluginОпределяется по типу sqlMybatisDataSourceInterceptorсвоего рода:

@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) {
    }
}

Установите источник данных для чтения в DataSourceHolder, только если он не находится в транзакции, а вызываемый sql имеет тип select
В других случаяхAbstractRoutingDataSourceБудет использовать источник данных записи по умолчанию

На данный момент проект может автоматически переключаться между чтением и записью источников данных без изменения исходного бизнес-кода.
Наконец, предоставьте демонстрацию для использования версии зависимостей.

<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>