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();
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;
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);
}
}
示例讲解
配置
数据库配置
- 在
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
插件,确保在应用启动时正确初始化读写分离的数据库连接。 - 在启动过程中,通过
TioBootServer.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
来管理数据库的读写操作。