springboot+mybatis實現數據庫的讀寫分離

介紹

隨着業務的發展,除了拆分業務模塊外,數據庫的讀寫分離也是常見的優化手段。
方案使用了AbstractRoutingDataSourcemybatis 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();
    }
}
複製代碼

實現AbstractRoutingDataSource

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

}
複製代碼

實現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)) {
                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>
複製代碼
相關文章
相關標籤/搜索