springboot 读写分离_springboot+mybatis实现数据库的读写分离

springboot 读写分离_springboot+mybatis实现数据库的读写分离_第1张图片

随着系统用户访问量的不断增加,数据库的频繁访问将成为我们系统的一大瓶颈之一。由于项目前期用户量不大,我们实现单一的数据库就能完成。但是后期单一的数据库根本无法支撑庞大的项目去访问数据库,那么如何解决这个问题呢?

采用读写分离技术的目标:有效减轻Master库的压力,又可以把用户查询数据的请求分发到不同的Slave库,从而保证系统的健壮性

springboot 读写分离_springboot+mybatis实现数据库的读写分离_第2张图片

介绍

随着业务的发展,除了拆分业务模块外,数据库的读写分离也是常见的优化手段。

方案使用了AbstractRoutingDataSource和mybatis plugin来动态的选择数据源

选择这个方案的原因主要是不需要改动原有业务代码,非常友好

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

springboot 读写分离_springboot+mybatis实现数据库的读写分离_第3张图片

环境

首先,我们需要两个数据库实例,一为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

springboot 读写分离_springboot+mybatis实现数据库的读写分离_第4张图片

实现

整个实现主要有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();
    }
}

springboot 读写分离_springboot+mybatis实现数据库的读写分离_第5张图片

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

springboot 读写分离_springboot+mybatis实现数据库的读写分离_第6张图片

实现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数据源

springboot 读写分离_springboot+mybatis实现数据库的读写分离_第7张图片

至此,项目已经可以自动的在读、写数据源间切换,无需修改原有的业务代码

最后,提供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
    

springboot 读写分离_springboot+mybatis实现数据库的读写分离_第8张图片


------本文完结-------

感谢你的阅读,如果喜欢的话评论、转发一下再走吧!!!

以后会有更多精彩内容呈现欢迎关注!!!!

你可能感兴趣的:(springboot,读写分离)