springbootmybatis实现数据库的读写分离

38次阅读

共计 6256 个字符,预计需要花费 16 分钟才能阅读完成。

介绍

随着业务的发展,除了拆分业务模块外,数据库的读写分离也是常见的优化手段。
方案使用了 AbstractRoutingDataSourcemybatis 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<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()方法返回了需要使用的数据源对应的 key

当前线程需要使用的数据源对应的 key,是在 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 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 使用依赖版本

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

正文完
 0