1. 为什么需要多数据源和连接池?

在真实的项目开发里,尤其是稍微有点规模的企业级应用,你很少会只跟一个数据库打交道。我见过不少项目,它们的情况往往是这样的:用户的核心业务数据放在一个MySQL主库里,日志和操作记录为了不影响主库性能,会扔到另一个MySQL或者PostgreSQL里;同时,可能还需要从公司另一个老旧的ERP系统(用的可能是Oracle或SQL Server)里同步一些基础数据。这时候,你的Java应用就需要同时连接多个数据库,这就是所谓的“多数据源”场景。

如果还像入门时那样,每次操作都临时创建数据库连接,用完就关,那性能绝对是灾难。想象一下,一个促销活动页面,每秒有几千次请求,每个请求都要经历“建立TCP连接 -> 数据库认证 -> 执行SQL -> 关闭连接”这个漫长的过程,数据库服务器光应付连接握手就累垮了。所以,我们得用“连接池”。

连接池就像个“数据库连接管家”。它一开始就创建好一批连接放在池子里备用。当你的程序需要操作数据库时,直接从池子里借一个现成的连接来用,用完了不是真的关闭,而是还回池子里,留给下一个请求用。这样就避免了频繁创建和销毁连接的巨大开销。市面上优秀的连接池实现很多,比如HikariCP(以快和轻量著称,Spring Boot的默认选择)、Druid(阿里出品,功能全面,自带监控)等。

而Hutool-db这个工具,它在JDBC之上做了非常友好的封装,让你用类似ActiveRecord的简单方式操作数据库。更棒的是,它原生就支持了多数据源的配置,并且能够无缝集成这些主流的连接池。也就是说,你既享受了Hutool简化开发的便利,又能获得连接池带来的高性能和高可靠性。接下来,我就带你一步步搞定它。

2. 5分钟快速搭建多数据源环境

咱们先从最直观的配置文件方式开始,这是最常用、也最省心的办法。Hutool约定,在项目的 src/main/resources/config/ 目录下,放一个叫 db.setting 的文件。如果没有config目录,直接放在resources根目录下也行,Hutool都能找到。

这个文件的神奇之处在于,它可以用“分组”来区分不同的数据源。我们假设一个最典型的场景:你的应用要同时连接一个主业务库(MySQL)和一个记录日志的库(另一个MySQL,也可以是别的)。

首先,确保你的Maven项目引入了必要的依赖。除了Hutool,你还需要数据库驱动和你选定的连接池。这里我以最常用的 HikariCP 和 MySQL 为例:

<dependencies>
    <!-- Hutool 全套工具,包含db模块 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.16</version>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <!-- HikariCP 连接池 -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.0.1</version>
    </dependency>
</dependencies>

然后,我们来编写 config/db.setting 文件:

# 第一个数据源:主业务库,分组名为 [main_db]
[main_db]
# 基本连接信息
url = jdbc:mysql://localhost:3306/my_business_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username = root
password = your_password_here

# HikariCP 连接池专属配置
# 连接池中最大连接数
maximumPoolSize = 20
# 连接超时时间(毫秒)
connectionTimeout = 30000
# 连接最大存活时间(毫秒),建议比数据库wait_timeout稍小
maxLifetime = 1800000
# 连接空闲超时时间(毫秒)
idleTimeout = 600000
# 连接测试查询
connectionTestQuery = SELECT 1

# 第二个数据源:日志库,分组名为 [log_db]
[log_db]
url = jdbc:mysql://192.168.1.100:3306/operation_log_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username = log_user
password = log_user_password

# Log数据库的连接池配置可以单独调整,比如日志库压力小,池子可以小点
maximumPoolSize = 10
connectionTimeout = 30000
maxLifetime = 1800000
idleTimeout = 600000
connectionTestQuery = SELECT 1

# 以下是全局可选配置,对所有数据源生效(如果对应连接池支持)
# 是否在日志中显示执行的SQL(调试时非常有用)
showSql = true
# 是否格式化显示的SQL
formatSql = true
# 是否显示SQL参数
showParams = true
# 打印SQL的日志级别
sqlLevel = debug

看,配置文件的结构非常清晰。用 [groupName] 来定义一个数据源分组,下面跟着这个数据源的所有配置。Hutool会自动识别 url 里的数据库类型,并加载对应的驱动类,所以 driver 配置项通常可以省略。

2.1 如何获取并使用不同的数据源?

配置文件写好了,在代码里使用起来简单得不可思议。Hutool提供了一个 DSFactory(数据源工厂)类,它就是专门干这个的。

import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.db.ds.DSFactory;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.List;

public class MultiDataSourceDemo {
    public static void main(String[] args) {
        // 1. 通过分组名,从工厂获取指定的数据源
        DataSource mainDataSource = DSFactory.get("main_db");
        DataSource logDataSource = DSFactory.get("log_db");

        // 2. 使用 Db.use(DataSource) 来获取一个针对特定数据源的Db操作对象
        Db mainDb = Db.use(mainDataSource);
        Db logDb = Db.use(logDataSource);

        try {
            // 操作主库:查询用户
            List<Entity> userList = mainDb.query("SELECT * FROM user WHERE status = ?", 1);
            for (Entity user : userList) {
                System.out.println("主库用户:" + user.getStr("name"));
            }

            // 操作日志库:插入一条操作日志
            int logId = logDb.insertForGeneratedKey(
                Entity.create("operation_log")
                    .set("user_id", 1001)
                    .set("action", "LOGIN")
                    .set("ip", "192.168.1.1")
                    .set("create_time", new java.util.Date())
            );
            System.out.println("日志插入成功,ID: " + logId);

        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

代码是不是非常简洁?DSFactory.get(“分组名”) 这一行,就完成了连接池的创建、配置的注入等所有脏活累活。你拿到的是一个标准的 javax.sql.DataSource 对象,这意味着你不仅可以给Hutool用,也可以给任何其他需要 DataSource 的框架(比如Spring管理的MyBatis)使用。

3. 动态数据源切换:让代码更灵活

上面的方式是在代码里显式地指定用哪个 Db 对象。但在有些复杂的业务逻辑中,我们可能希望根据运行时的情况动态决定使用哪个数据源。比如,根据租户ID切换到对应的数据库,或者根据操作类型决定是读主库还是读从库。

Hutool通过 Db.use(DataSource) 已经给了我们基础能力,我们可以在此基础上构建自己的动态路由逻辑。这里我分享一个我项目中常用的、基于ThreadLocal的简单动态数据源上下文管理器。

import cn.hutool.db.Db;
import cn.hutool.db.ds.DSFactory;
import javax.sql.DataSource;

/**
 * 动态数据源上下文持有者
 */
public class DynamicDataSourceHolder {
    /**
     * 使用ThreadLocal来为每个线程独立保存数据源Key
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    // 预定义的数据源Key,应该与db.setting中的分组名一致
    public static final String DS_MAIN = "main_db";
    public static final String DS_LOG = "log_db";
    public static final String DS_READONLY = "readonly_db"; // 假设还有一个只读从库

    /**
     * 设置当前线程要使用的数据源Key
     */
    public static void setDataSourceKey(String dsKey) {
        CONTEXT_HOLDER.set(dsKey);
    }

    /**
     * 获取当前线程的数据源Key
     */
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get() == null ? DS_MAIN : CONTEXT_HOLDER.get(); // 默认主库
    }

    /**
     * 清除当前线程的数据源Key
     */
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }

    /**
     * 根据当前上下文,获取对应的Db操作对象
     * 这是一个便捷方法,实际使用中可能结合AOP
     */
    public static Db getCurrentDb() {
        String dsKey = getDataSourceKey();
        DataSource dataSource = DSFactory.get(dsKey);
        return Db.use(dataSource);
    }
}

有了这个上下文管理器,我们就可以在业务代码中灵活切换了。通常,我们会把切换逻辑放在Service层的方法入口,或者用AOP(面向切面编程)来实现,这样业务代码就干净了。这里先看一个手动切换的例子:

public class OrderService {
    public void createOrder(Order order) {
        // 默认会使用主库(DynamicDataSourceHolder的默认值)
        Db db = DynamicDataSourceHolder.getCurrentDb();

        try {
            // 1. 开启事务(Hutool的事务支持很方便)
            db.beginTransaction();
            
            // 2. 向主库插入订单
            db.insert(Entity.create("t_order").parseBean(order));
            
            // 3. 动态切换到日志库,记录操作日志(同一个事务内!注意这里的事务是主库的,跨库事务是分布式事务,更复杂)
            // 为了演示切换,我们先提交主库事务(实际场景需根据业务设计)
            db.commit();
            
            // 切换到日志库执行
            DynamicDataSourceHolder.setDataSourceKey(DynamicDataSourceHolder.DS_LOG);
            Db logDb = DynamicDataSourceHolder.getCurrentDb();
            logDb.insert(Entity.create("order_log")
                .set("order_id", order.getId())
                .set("action", "CREATE")
                .set("operator", order.getUserId()));
                
            // 4. 最后记得清理上下文,避免内存泄漏和后续操作污染
            DynamicDataSourceHolder.clearDataSourceKey();
            
        } catch (SQLException e) {
            // 异常处理,回滚事务等
            try { db.rollback(); } catch (SQLException ex) { ex.printStackTrace(); }
            e.printStackTrace();
        } finally {
            // 确保上下文被清理
            DynamicDataSourceHolder.clearDataSourceKey();
        }
    }
}

注意:这个例子中,订单插入和日志插入是在两个不同的数据库连接(甚至可能是两个不同的事务)中完成的。如果要求“订单创建和日志记录必须同时成功或同时失败”,那就涉及到分布式事务(如Seata)了,这比单数据源事务复杂得多。在大多数情况下,日志记录允许有一定的延迟或最终一致性,所以分开处理是常见做法。

4. 连接池优化:选型与调参实战

配置多数据源只是第一步,要让系统在高并发下稳定运行,连接池的优化至关重要。Hutool支持多种连接池,但怎么选?怎么调?这里我结合自己的踩坑经验,给你一些实在的建议。

4.1 连接池选型:HikariCP vs Druid

这是目前Java圈里最主流的两个选择。

  • HikariCP:口号是“快、简单、可靠”。它的代码量小,执行路径优化到了极致,在基准测试中性能通常是最好的。如果你的需求就是需要一个稳定高效的连接池,没有太多花哨的监控需求,那么 HikariCP 是首选。Spring Boot从2.0开始就把它作为默认连接池了。
  • Druid:阿里开源的功能怪兽。除了连接池基本功能,它还内置了强大的监控统计、SQL防注入、WallFilter、加密等特性。如果你需要详细监控SQL执行情况、慢查询、连接泄露,或者对安全性有较高要求,Druid 是更强大的选择。

在Hutool中使用它们,只需要引入对应的jar包,并在 db.setting 中配置相应的参数即可,Hutool会自动检测并创建正确的数据源类型。你不需要写任何额外的Java代码来初始化它们。

4.2 核心参数调优指南

无论选择哪个连接池,以下几个参数都是调优的重点。我把它们整理成了一个表格,方便你对照理解:

参数名含义与作用调优建议与常见坑
maximumPoolSize (Hikari)
maxActive (Druid)
连接池中允许的最大连接数。这是最重要的参数之一。不是越大越好! 每个连接都会占用数据库和应用的资源。一个参考公式:(CPU核心数 * 2) + 磁盘数。对于Web应用,初始可以设为 10-20,根据监控调整。设置过大会压垮数据库。
minimumIdle (Hikari)
minIdle (Druid)
连接池中保持的最小空闲连接数。HikariCP官方建议不设置(等于 maximumPoolSize),让它根据需要创建。Druid可以设置一个较小的值(如5),维持基本连接。
connectionTimeout从池中获取连接的最大等待时间(毫秒)。默认30秒(30000)太长,线上应用建议设为 2-5秒(2000-5000)。超时拿不到连接应快速失败,而不是长时间等待。
maxLifetime一个连接在池中的最大存活时间。数据库服务端(如MySQL的wait_timeout)会强制关闭空闲连接。此值应略小于数据库的wait_timeout(默认8小时)。建议设为 30分钟(1800000)到 2小时。
idleTimeout连接在池中空闲的最大时间,超时将被回收。建议设为 10分钟(600000)。如果 minimumIdle 和 maximumPoolSize 相等,此参数可能不生效。
validationQuery用于测试连接是否有效的简单SQL。必须是极快的查询,如 SELECT 1。Druid可配,HikariCP有默认值。

4.3 为不同数据源设置不同的池策略

在我们的多数据源配置里,main_db(主业务库)和 log_db(日志库)的压力模型是完全不同的。主库可能面临高频的随机读写,而日志库主要是顺序写入。因此,它们的连接池配置也应该区别对待。

[main_db]
url = jdbc:mysql://localhost:3306/main_db
username = ...
password = ...
# 主库压力大,连接池可以大一些,连接等待要短
maximumPoolSize = 25
connectionTimeout = 2000
maxLifetime = 1800000
idleTimeout = 600000
# 开启监控统计(Druid特有)
filters = stat,wall

[log_db]
url = jdbc:mysql://localhost:3306/log_db
username = ...
password = ...
# 日志库写入压力相对平稳,连接池可以小很多
maximumPoolSize = 10
connectionTimeout = 5000 # 可以稍长一点
maxLifetime = 1800000
idleTimeout = 1200000 # 空闲时间可以更长,因为写入可能是一阵一阵的
# 日志库的SQL可能比较固定,可以开启PreparedStatement缓存提升性能(Druid)
poolPreparedStatements = true
maxOpenPreparedStatements = 20

通过这种差异化的配置,可以让每个数据源的资源分配更加合理,避免日志库占用过多连接资源,影响核心业务。

5. 高级技巧:纯代码配置与自定义扩展

虽然配置文件方式很优雅,但有些时候,我们的数据源信息可能是从配置中心(如Nacos、Apollo)动态获取的,或者需要更复杂的初始化逻辑。这时候,我们就需要纯代码的方式来配置数据源了。

5.1 不使用配置文件,手动创建数据源

你可以完全抛开 db.setting,像传统方式一样创建连接池对象,然后交给Hutool的 Db 使用。

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import java.sql.SQLException;

public class CodeConfigDemo {
    public static void main(String[] args) throws SQLException {
        // 1. 手动配置并创建 HikariCP 数据源
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
        config.setUsername("root");
        config.setPassword("password");
        config.setMaximumPoolSize(15);
        config.setConnectionTimeout(3000);
        config.setMaxLifetime(1800000);
        // ... 其他HikariCP参数
        
        HikariDataSource dataSource = new HikariDataSource(config);
        
        // 2. 将手动创建的数据源交给 Db 使用
        Db db = Db.use(dataSource);
        
        // 3. 执行操作
        Entity result = db.queryOne("SELECT 'Hello from Code Config' as msg");
        System.out.println(result.getStr("msg"));
        
        // 4. 注意:手动创建的数据源需要自己在合适的时机(如应用关闭时)调用 dataSource.close()
        // dataSource.close();
    }
}

5.2 自定义数据源工厂

如果你希望统一管理这种代码创建的数据源,或者想集成一个Hutool默认不支持的连接池(比如 tomcat-jdbc),你可以实现自己的 DSFactory。

import cn.hutool.db.ds.DSFactory;
import cn.hutool.setting.Setting;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.apache.tomcat.jdbc.pool.PoolProperties;
import javax.sql.DataSource;

/**
 * 自定义Tomcat JDBC Pool的工厂
 */
public class TomcatDSFactory extends DSFactory {
    
    public TomcatDSFactory() {
        this(null);
    }
    
    public TomcatDSFactory(Setting setting) {
        super("Tomcat-JDBC-Pool", setting);
    }

    @Override
    public DataSource createDataSource(String jdbcUrl, String username, String password, String driver) {
        // 1. 创建Tomcat JDBC Pool的配置
        PoolProperties poolProps = new PoolProperties();
        poolProps.setUrl(jdbcUrl);
        poolProps.setUsername(username);
        poolProps.setPassword(password);
        if (null != driver) {
            poolProps.setDriverClassName(driver);
        }
        
        // 2. 可以从全局Setting中读取更多自定义配置(比如分组下的poolSize等)
        // 这里简单设置几个
        poolProps.setInitialSize(5);
        poolProps.setMaxActive(20);
        poolProps.setTestOnBorrow(true);
        poolProps.setValidationQuery("SELECT 1");
        
        // 3. 创建并返回数据源
        return new org.apache.tomcat.jdbc.pool.DataSource(poolProps);
    }

    // 提供一个全局设置自定义工厂的方法(通常在应用启动时调用一次)
    public static void setAsGlobal() {
        DSFactory.setCurrentDSFactory(new TomcatDSFactory());
    }
}

在应用启动时,调用 TomcatDSFactory.setAsGlobal(),之后 DSFactory.get() 就会使用你自定义的工厂来创建数据源了。这种方式给了你最大的灵活性。

6. 生产环境注意事项与排坑经验

最后,结合我这些年趟过的坑,给你几点在生产环境使用Hutool多数据源和连接池的忠告。

第一,一定要监控连接池。 特别是使用Druid时,务必开启它的内置监控。你可以通过一个简单的Servlet就能看到活跃连接数、等待线程数、执行过的SQL、慢查询等关键信息。很多性能问题(如连接泄露、慢SQL拖垮池子)都是通过监控第一时间发现的。HikariCP也可以通过JMX或Micrometer等指标库进行监控。

第二,妥善处理连接泄露。 这是最常见的问题。表现就是连接池的连接数慢慢涨到 maximumPoolSize 后,新的请求全部卡在 connectionTimeout 上。确保你的代码在 finally 块中关闭了 Db 查询返回的 ResultSet、Statement,或者更简单地,确保每一次 Db.use() 获取的连接(在Hutool的封装下)都能被正确归还。Hutool的 Db 方法内部通常会处理好资源关闭,但如果你手动获取了 Connection,务必用 DbUtil.close(connection)。

第三,为不同环境准备不同的配置。 开发、测试、生产环境的数据库地址、性能要求都不同。不要用一个 db.setting 走天下。可以利用Maven的profile功能,或者Spring Boot的 application-{profile}.properties 机制,来切换不同环境的配置文件。Hutool的 DSFactory 也支持通过编程方式加载指定路径的Setting文件。

第四,谨慎使用跨库事务。 正如前面提到的,Hutool的 db.beginTransaction() 是针对单个 Db 对象(即单个数据源)的。如果你在一个业务里先后操作了A库和B库,然后回滚,只有A库的操作会被回滚,B库的已经提交了。真正的分布式事务需要引入额外的中间件(如Seata),设计上也会复杂很多,很多时候我们会通过最终一致性方案(如消息队列)来规避。

第五,做好数据源降级和熔断。 对于日志库、报表库等非核心数据源,如果它们所在的数据库宕机了,不应该让核心业务流程因此挂掉。可以考虑在获取 DataSource 或 Db 对象的地方加入熔断器(如Resilience4j),当失败率达到阈值时,自动降级,比如将日志暂时写入本地文件或内存队列,等数据库恢复后再同步。

把这些点都考虑到,你的基于Hutool的多数据源应用就能在企业级项目中稳稳当当地跑起来了。说到底,工具再好,也只是工具,理解其背后的原理和最佳实践,才能真的让它为你所用,而不是被它牵着鼻子走。

更多推荐