java-db 读写分离

java-db 的Db 工具类内置了读写分离功能,读操作会自动切换到读数据源,写操作则会自动切换到写数据源。

  • 当调用 Db.findDb.queryDb.paginate 等方法时,系统会自动执行读操作。
  • 其他方法则用于执行写操作。

使用示例

配置

app.properties 文件配置示例:

DATABASE_DSN=postgresql://postgres:123456@192.168.1.2/defaultdb
DATABASE_DSN_REPLICAS=postgresql://postgres:123456@192.168.1.3/defaultdb,postgresql://postgres:123456@192.168.1.4/defaultdb
  • DATABASE_DSN:写库连接字符串。
  • DATABASE_DSN_REPLICAS:读库连接字符串,多个读库用逗号 , 分隔。

配置类

import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;

import com.jfinal.template.Engine;
import com.jfinal.template.source.ClassPathSourceFactory;
import com.litongjava.db.activerecord.ActiveRecordPlugin;
import com.litongjava.db.activerecord.OrderedFieldContainerFactory;
import com.litongjava.db.activerecord.ReplicaActiveRecordPlugin;
import com.litongjava.db.activerecord.dialect.PostgreSqlDialect;
import com.litongjava.db.hikaricp.DsContainer;
import com.litongjava.jfinal.aop.annotation.AConfiguration;
import com.litongjava.jfinal.aop.annotation.AInitialization;
import com.litongjava.tio.boot.constatns.TioBootConfigKeys;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.utils.dsn.DbDSNParser;
import com.litongjava.tio.utils.dsn.JdbcInfo;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

@AConfiguration
public class DbConfig {

  @AInitialization
  public void config() {
    configMain();
    configReplica();
  }

  private void configReplica() {
    String replicas = EnvUtils.get("DATABASE_DSN_REPLICAS");
    if (replicas == null) {
      return;
    }
    String[] dsns = replicas.split(",");
    List<DataSource> datasources = new ArrayList<>();
    for (String dsn : dsns) {
      JdbcInfo jdbc = new DbDSNParser().parse(dsn);
      int maximumPoolSize = EnvUtils.getInt("jdbc.MaximumPoolSize", 1);
      HikariConfig config = new HikariConfig();
      config.setJdbcUrl(jdbc.getUrl());
      config.setUsername(jdbc.getUser());
      config.setPassword(jdbc.getPswd());
      config.setMaximumPoolSize(maximumPoolSize);
      DataSource datasource = new HikariDataSource(config);
      datasources.add(datasource);
    }

    ReplicaActiveRecordPlugin replicaConfig = new ReplicaActiveRecordPlugin(datasources);
    replicaConfig.setContainerFactory(new OrderedFieldContainerFactory());
    if (EnvUtils.isDev()) {
      replicaConfig.setDevMode(true);
    }
    replicaConfig.setDialect(new PostgreSqlDialect());

    replicaConfig.start();
    TioBootServer.me().addDestroyMethod(replicaConfig::stop);
  }

  private DataSource mainDataSource() {
    String dsn = EnvUtils.get(TioBootConfigKeys.DATABASE_DSN);
    if (dsn == null) {
      return null;
    }

    JdbcInfo jdbc = new DbDSNParser().parse(dsn);
    int maximumPoolSize = EnvUtils.getInt("jdbc.MaximumPoolSize", 2);

    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(jdbc.getUrl());
    config.setUsername(jdbc.getUser());
    config.setPassword(jdbc.getPswd());
    config.setMaximumPoolSize(maximumPoolSize);

    HikariDataSource hikariDataSource = new HikariDataSource(config);

    DsContainer.setDataSource(hikariDataSource);
    TioBootServer.me().addDestroyMethod(hikariDataSource::close);
    return hikariDataSource;
  }

  private void configMain() {
    DataSource dataSource = mainDataSource();
    if (dataSource == null) {
      return;
    }
    ActiveRecordPlugin arp = new ActiveRecordPlugin(dataSource);
    arp.setContainerFactory(new OrderedFieldContainerFactory());
    if (EnvUtils.isDev()) {
      arp.setDevMode(true);
    }

    arp.setDialect(new PostgreSqlDialect());

    Engine engine = arp.getEngine();
    engine.setSourceFactory(new ClassPathSourceFactory());
    engine.setCompressorOn(' ');
    engine.setCompressorOn('\n');

    arp.start();
    TioBootServer.me().addDestroyMethod(arp::stop);
  }
}

测试类

import org.junit.Test;

import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Record;
import com.litongjava.tio.utils.environment.EnvUtils;
import com.red.book.config.DbConfig;

public class ServiceTest {

  @Test
  public void testService() {
    EnvUtils.load();
    try {
      new DbConfig().config();
    } catch (Exception e) {
      e.printStackTrace();
    }

    long start = System.currentTimeMillis();
    try {
      Long countTable = Db.countTable("student");
      for (int i = 0; i < countTable; i++) {
        String sql = "select * from student limit 1";
        Record record = Db.findFirst(sql);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println(end - start);
  }
}

示例讲解

配置

  1. 数据库配置

    • app.properties 文件中,定义了两个关键配置项:
      • DATABASE_DSN:写库的连接字符串。
      • DATABASE_DSN_REPLICAS:读库的连接字符串,多个读库用逗号 , 分隔。
  2. 主数据源配置

    • mainDataSource() 方法配置写库的 DataSource。使用 HikariCP 连接池管理数据库连接。
    • 解析 DATABASE_DSN 获取连接信息,然后通过 HikariConfig 配置连接池的相关参数(如最大连接数、URL、用户名、密码等)。
    • 最终将配置好的 DataSource 设置到 DsContainer 中,并添加到 TioBootServer 的销毁方法列表中,以确保程序结束时正确关闭连接。
  3. 读数据源配置

    • configReplica() 方法配置读库的 DataSource。首先从 DATABASE_DSN_REPLICAS 中读取配置,然后分割多个 DSN 并逐个解析。
    • 对每个解析后的 DSN,创建 HikariDataSource 并将其添加到一个 List<DataSource> 中。
    • 最后创建 ReplicaActiveRecordPlugin,并将读库的数据源列表传递给它,用于执行读操作。该插件的 start() 方法会启动配置好的读库连接。
  4. ActiveRecordPlugin 配置

    • configMain() 方法中,使用主数据源(写库)创建并启动 ActiveRecordPlugin,它负责管理数据库操作的 ORM(对象关系映射)部分。
    • ActiveRecordPlugin 启动后,还可以配置模板引擎(如 SQL 模板文件)。

启动过程

  • DbConfig 类的 @AInitialization 注解标记的方法 config() 中,分别调用 configMain()configReplica() 方法,完成主库(写库)和从库(读库)的配置。
  • 启动 ActiveRecordPluginReplicaActiveRecordPlugin 插件,确保在应用启动时正确初始化读写分离的数据库连接。
  • 在启动过程中,通过 TioBootServer.me().addDestroyMethod(...) 为每个配置好的数据源添加销毁方法,保证程序关闭时释放资源。

运行流程

  1. 加载环境配置

    • 在测试类中,通过 EnvUtils.load() 加载环境配置文件(如 app.properties),确保 DATABASE_DSNDATABASE_DSN_REPLICAS 的配置可用。
  2. 初始化数据库配置

    • 调用 new DbConfig().config() 来初始化配置,即启动读写分离的数据库连接池。
  3. 执行数据库操作

    • 使用 Db.countTable("student") 获取 student 表的记录数量(这通常是一个读操作,因此会使用配置好的读库)。
    • 循环执行 Db.findFirst(sql),每次查询一条记录。由于 Db.findFirst 是一个读操作,因此会路由到读库去执行。
  4. 性能测试

    • 通过记录开始时间和结束时间,计算整个操作的执行时间。

总结

  • 配置过程:通过 app.properties 文件配置主库和从库的 DSN,并在代码中分别配置主库和从库的数据源。通过 HikariCP 实现连接池管理。
  • 启动过程:在

应用启动时,通过 DbConfig 类初始化并启动主库和从库的 ActiveRecordPlugin,实现读写分离功能。

  • 运行流程:在运行时,读操作会自动路由到从库,而写操作会路由到主库。通过单元测试验证读操作的性能。

这个例子展示了如何在 JFinal 框架中实现读写分离,并通过 ActiveRecordPlugin 来管理数据库的读写操作。