java-db 读写分离
java-db 的Db 工具类内置了读写分离功能,读操作会自动切换到读数据源,写操作则会自动切换到写数据源。
- 当调用 Db.find、Db.query、Db.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 {
  @Initialization
  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();
    HookCan.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);
    HookCan.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();
    HookCan.me().addDestroyMethod(arp::stop);
  }
}
测试类
import org.junit.Test;
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.tio.utils.environment.EnvUtils;
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";
        Row row = Db.findFirst(sql);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println(end - start);
  }
}
示例讲解
配置
- 数据库配置 - 在 app.properties文件中,定义了两个关键配置项:- DATABASE_DSN:写库的连接字符串。
- DATABASE_DSN_REPLICAS:读库的连接字符串,多个读库用逗号- ,分隔。
 
 
- 在 
- 主数据源配置 - mainDataSource()方法配置写库的- DataSource。使用- HikariCP连接池管理数据库连接。
- 解析 DATABASE_DSN获取连接信息,然后通过HikariConfig配置连接池的相关参数(如最大连接数、URL、用户名、密码等)。
- 最终将配置好的 DataSource设置到DsContainer中,并添加到TioBootServer的销毁方法列表中,以确保程序结束时正确关闭连接。
 
- 读数据源配置 - configReplica()方法配置读库的- DataSource。首先从- DATABASE_DSN_REPLICAS中读取配置,然后分割多个 DSN 并逐个解析。
- 对每个解析后的 DSN,创建 HikariDataSource并将其添加到一个List<DataSource>中。
- 最后创建 ReplicaActiveRecordPlugin,并将读库的数据源列表传递给它,用于执行读操作。该插件的start()方法会启动配置好的读库连接。
 
- ActiveRecordPlugin 配置 - configMain()方法中,使用主数据源(写库)创建并启动- ActiveRecordPlugin,它负责管理数据库操作的 ORM(对象关系映射)部分。
- 在 ActiveRecordPlugin启动后,还可以配置模板引擎(如 SQL 模板文件)。
 
启动过程
- 在 DbConfig类的@Initialization注解标记的方法config()中,分别调用configMain()和configReplica()方法,完成主库(写库)和从库(读库)的配置。
- 启动 ActiveRecordPlugin和ReplicaActiveRecordPlugin插件,确保在应用启动时正确初始化读写分离的数据库连接。
- 在启动过程中,通过 HookCan.me().addDestroyMethod(...)为每个配置好的数据源添加销毁方法,保证程序关闭时释放资源。
运行流程
- 加载环境配置 - 在测试类中,通过 EnvUtils.load()加载环境配置文件(如app.properties),确保DATABASE_DSN和DATABASE_DSN_REPLICAS的配置可用。
 
- 在测试类中,通过 
- 初始化数据库配置 - 调用 new DbConfig().config()来初始化配置,即启动读写分离的数据库连接池。
 
- 调用 
- 执行数据库操作 - 使用 Db.countTable("student")获取student表的记录数量(这通常是一个读操作,因此会使用配置好的读库)。
- 循环执行 Db.findFirst(sql),每次查询一条记录。由于Db.findFirst是一个读操作,因此会路由到读库去执行。
 
- 使用 
- 性能测试 - 通过记录开始时间和结束时间,计算整个操作的执行时间。
 
总结
- 配置过程:通过 app.properties文件配置主库和从库的 DSN,并在代码中分别配置主库和从库的数据源。通过HikariCP实现连接池管理。
- 启动过程:在
应用启动时,通过 DbConfig 类初始化并启动主库和从库的 ActiveRecordPlugin,实现读写分离功能。
- 运行流程:在运行时,读操作会自动路由到从库,而写操作会路由到主库。通过单元测试验证读操作的性能。
这个例子展示了如何在 JFinal 框架中实现读写分离,并通过 ActiveRecordPlugin 来管理数据库的读写操作。
