欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

spring boot + mybatis如何实现数据库的读写分离

程序员文章站 2022-05-04 19:18:49
介绍 随着业务的发展,除了拆分业务模块外,数据库的读写分离也是常见的优化手段。 方案使用了abstractroutingdatasource和mybatis...

介绍

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

方案使用了abstractroutingdatasource和mybatis 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>

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。