java 数据库连接池配置读写分离的核心在于自定义 datasource 实现,通过拦截 getconnection() 方法并根据 sql 类型(读或写)动态路由到对应的物理数据源。具体做法是继承 abstractdatasource 或使用 aop 技术,在获取连接前解析 sql 语句,若为查询类操作则从只读库获取连接,否则从主库获取连接。

如何基于 SQL 关键字实现简单的读写路由
最简单的实现方式是检查传入的 SQL 字符串是否以 SELECT 开头。如果是,则返回从 readDataSource 获取的连接;如果不是,则返回从 writeDataSource 获取的连接。这种方法无需引入复杂的中间件,适合对性能要求不高且 SQL 规范的项目。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
public class SimpleRoutingDataSource {
private String readUrl = "jdbc:mysql://localhost:3306/read_db";
private String writeUrl = "jdbc:mysql://localhost:3306/write_db";
private String user = "root";
private String password = "password";
public Connection getConnection(String sql) throws SQLException {
// 简单判断:如果SQL包含SELECT,则走读库
if (sql != null && sql.trim().toUpperCase().startsWith("SELECT")) {
return DriverManager.getConnection(readUrl, user, password);
} else {
return DriverManager.getConnection(writeUrl, user, password);
}
}
}上述代码展示了最基础的逻辑。在实际应用中,直接调用 DriverManager 并不具备连接池特性,因此通常需要将此逻辑嵌入到现有的连接池(如 HikariCP)之上,或者使用现成的支持读写分离的组件如 ShardingSphere-JDBC。但核心思想一致:根据 SQL 意图选择数据源。
立即学习“Java免费学习笔记(深入)”;
如何结合 MyBatis 插件实现更精准的读写分离
在 Spring Boot + MyBatis 项目中,可以通过实现 Interceptor 接口来拦截 SQL 执行前的参数处理。这种方式比单纯判断字符串更可靠,因为可以解析 BoundSql 对象,准确识别查询语句。我们需要定义一个自定义注解或配置项,标记哪些 Mapper 方法需要走读库,或者自动推断所有非更新操作为读操作。

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.Properties;
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class ReadWriteSplittingInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
Field delegateField = statementHandler.getClass().getDeclaredField("delegate");
delegateField.setAccessible(true);
RoutingStatementHandler routingHandler = (RoutingStatementHandler) delegateField.get(statementHandler);
BoundSql boundSql = routingHandler.getBoundSql();
String sql = boundSql.getSql();
// 这里可以注入一个 DataSourceRouter 来决定使用哪个数据源
// 假设我们有一个静态方法根据SQL返回数据源
DataSourceRouter.route(sql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 初始化配置
}
}该插件会在 SQL 执行前被触发。开发者需要在 DataSourceRouter 中维护当前线程的数据源上下文。MyBatis 的插件机制允许我们在底层 JDBC 调用之前介入,从而决定最终使用的 Connection 来源。这种方式比纯字符串匹配更安全,因为它利用了 MyBatis 内部的解析结果。
当主从库延迟导致读取旧数据时如何解决
读写分离最大的痛点是主从复制延迟。如果在写入后立即进行读取,可能从库尚未同步最新数据。解决这一问题的具体做法是在关键业务场景中提供“强制读主”的能力。例如,在事务开始时或写入操作后,将当前线程的数据源标记为主库,直到事务结束或显式切换。
在之前的 SimpleRoutingDataSource 基础上,我们可以增加一个 ThreadLocal 来存储强制路由标志:
public class AdvancedRoutingDataSource {
private static final ThreadLocal<Boolean> FORCE_READ_MASTER = new ThreadLocal<>();
// ... 其他字段和getConnection方法 ...
public void forceReadMaster() {
FORCE_READ_MASTER.set(true);
}
public void clearForceReadMaster() {
FORCE_READ_MASTER.remove();
}
public Connection getConnection(String sql) throws SQLException {
// 如果强制读主,无论SQL是什么都走写库
if (Boolean.TRUE.equals(FORCE_READ_MASTER.get())) {
return DriverManager.getConnection(writeUrl, user, password);
}
if (sql != null && sql.trim().toUpperCase().startsWith("SELECT")) {
return DriverManager.getConnection(readUrl, user, password);
} else {
return DriverManager.getConnection(writeUrl, user, password);
}
}
}在使用时,开发者可以在 Service 层的关键逻辑前后调用 forceReadMaster() 和 clearForceReadMaster()。例如,在支付成功后立即查询订单状态,应确保这次查询走主库,以保证数据一致性。这种策略牺牲了部分读库的负载均衡优势,但保证了强一致性场景下的正确性。


















