一个MySQL实例同时承载订单、用户、日志等多个数据库,是很多项目早期的常见选择。随着业务模块增多,应用侧要同时读写这些库,连接管理就开始变得棘手。如果给每个库都单独建一个连接池,连接数会成倍增加;如果所有模块共用一个池子,一个慢查询又可能拖垮全局。下面围绕单实例多数据库场景,聊聊连接池怎么拆、账号怎么隔离、事务怎么处理,以及该盯哪些指标。

一、先弄清楚连接与Schema的关系
MySQL里的数据库和Schema在概念上基本可以画等号。一个物理连接在建立时通常会指定一个默认数据库,比如jdbc:mysql://127.0.0.1:3306/order_db;但连接本身并不绑定这个库,后续完全可以通过USE user_db切换,或者直接在SQL里用全限定表名跨库访问。
这意味着单实例多数据库场景下,并不需要为每个库都保持一条常驻连接。如果两个库的访问频率都很低,共享一个连接池完全可行。真正影响连接分配的是事务隔离和业务优先级,而不是库的数量。比如订单库的写操作必须及时提交,日志库的批量插入可以适当排队,如果硬塞进同一个池子,日志写入占满连接时,订单服务就会被迫等待。
另外,全限定表名比USE切换更适合生产环境。频繁切换默认数据库会让连接状态变得不透明,尤其是在连接池复用连接时,前一个线程切换过的库可能影响后一个使用者的预期。统一使用库名.表名的方式,可以让SQL意图更清晰,也减少连接管理上的隐性耦合。
-- 不推荐:依赖USE切换默认库 USE user_db; SELECT id, nickname FROM users WHERE id = 1001; -- 推荐:使用全限定表名 SELECT id, nickname FROM user_db.users WHERE id = 1001;
二、连接池拆分策略:共用还是分开
拆分连接池之前,先算一笔账。MySQL实例的max_connections通常默认151,扣除系统内部线程后,留给业务连接的可能只有130个左右。假设有订单、用户、日志、报表四个库,每个库建一个池子,每个池子maximum-pool-size设为20,那么应用最多会创建80个连接。看起来没超,但再加上多实例部署、后台任务和监控探针,很容易触碰上限。
更合理的做法是按业务隔离级别拆分。核心交易链路的数据库可以单独建池,配置合理的maximum-pool-size和较短的connectionTimeout;日志、报表这类离线查询可以共用一个池子,设置较小的最大连接数,避免影响在线业务。比如订单库单独20个连接,用户库单独10个,日志和报表共享10个,总计40个,比每库20个少了一半。
如果多个库的访问频率都很低,也可以共用一个连接池,在SQL中使用全限定表名。这样做的好处是连接复用率高,连接数最省;坏处是连接池参数只能全局生效,无法按库设置超时和连接数上限。一旦某个库出现慢查询,连接池可能被慢SQL占满。所以共用池适用于访问量小、查询模式接近的库,不适合核心库与报表库混用。
下面是一个使用HikariCP配置两个独立数据源的示例,订单库和用户库分别管理:
spring:
datasource:
order:
jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db?useSSL=false&serverTimezone=UTC
username: order_app
password: order_pass
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
user:
jdbc-url: jdbc:mysql://127.0.0.1:3306/user_db?useSSL=false&serverTimezone=UTC
username: user_app
password: user_pass
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
如果项目里没有用Spring Boot的多数据源自动装配,也可以通过代码创建HikariDataSource。核心是把每个业务库的池参数独立出来,避免相互影响。需要注意的是,所有池子的最大连接数加总后要预留20%到30%的余量给临时连接和运维操作。
三、通过账号权限实现连接隔离
连接管理不光是技术参数,还涉及权限边界。单实例多库最容易出现的问题是一个应用账号拥有所有库的读写权限。开发环境可能无所谓,一旦到了生产环境,某个模块的SQL拼接漏洞或误操作就可能影响全实例。更好的做法是给每个业务模块创建独立账号,只授予对应库的权限。
例如订单服务使用order_app账号,只允许操作order_db;用户服务使用user_app账号,只允许操作user_db。这样即使订单服务的连接被拖垮或被注入攻击,用户库的数据仍然受到保护。MySQL的库级授权非常成熟,可以通过GRANT语句精确控制SELECT、INSERT、UPDATE、DELETE等权限。
CREATE USER 'order_app'@'%' IDENTIFIED BY 'Order@2024'; GRANT SELECT, INSERT, UPDATE, DELETE ON order_db.* TO 'order_app'@'%'; CREATE USER 'user_app'@'%' IDENTIFIED BY 'User@2024'; GRANT SELECT, INSERT, UPDATE ON user_db.* TO 'user_app'@'%'; FLUSH PRIVILEGES;
账号隔离与连接池拆分要配合使用。每个数据源使用自己的账号密码,这样连接池层面的隔离和数据库权限层面的隔离可以对应起来。排查问题时,通过performance_schema或processlist也能快速定位是哪个账号占用了连接、执行了哪些SQL。不要为了省事创建一个拥有ALL PRIVILEGES的通用账号,那样单实例多库的风险会被放大。
四、跨库查询与事务边界要弄清楚
同一个MySQL实例下的跨库查询本质上是跨Schema查询,不涉及分布式事务协议。InnoDB事务的作用范围是整个实例,所以在一个事务里同时修改order_db和user_db的数据,提交或回滚是可以保证的。但问题在于,跨库事务会让连接持有时间变长,锁范围可能扩展,进而加剧连接池的争用。
如果业务允许,尽量避免在核心链路中使用跨库事务。例如用户注册后送积分,可以把用户信息写入user_db后,通过消息队列通知积分库异步处理。这样两个库的操作拆成独立事务,连接占有时长更短,连接池压力更小。对于必须保证一致性的场景,可以评估使用本地消息表或补偿任务,而不是把多个库的写操作都塞进同一个大事务。
如果确实要使用跨库事务,代码里要控制好事务边界,不要在事务内做远程调用、发邮件、查询报表等耗时操作。下面是一个简单的JDBC事务示例,注意两个UPDATE都使用全限定表名:
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement updateOrder = conn.prepareStatement(
"UPDATE order_db.orders SET status = ? WHERE order_id = ?")) {
updateOrder.setString(1, "PAID");
updateOrder.setLong(2, 1001L);
updateOrder.executeUpdate();
}
try (PreparedStatement updateUser = conn.prepareStatement(
"UPDATE user_db.users SET balance = balance - ? WHERE user_id = ?")) {
updateUser.setBigDecimal(1, new BigDecimal("99.00"));
updateUser.setLong(2, 2001L);
updateUser.executeUpdate();
}
conn.commit();
} catch (SQLException e) {
// 回滚由连接层面处理
}
上面事务的提交和回滚都是基于同一个连接,只要没有中间件把SQL路由到不同实例,就能保证原子性。但需要注意,连接池会复用连接,如果事务中途连接被关闭或超时回收,必须确保连接池的connectionInitSql或autoCommit设置正确,否则可能出现事务残留。
五、监控指标与参数调优
连接管理做得好不好,最终要落到监控上。MySQL侧可以重点关注 Threads_connected、Threads_running 和 max_connections。Threads_connected 持续接近 max_connections,说明连接数已经到瓶颈,要么调大上限,要么减少连接池大小。Threads_running 长期大于CPU核数,通常意味着SQL执行效率或索引有问题。
连接池侧则要关注活跃连接、空闲连接、等待线程数和连接创建耗时。HikariCP的poolName、maximumPoolSize、connectionTimeout、idleTimeout、maxLifetime、leakDetectionThreshold这几个参数值得花时间调。maximumPoolSize不是越大越好,过大只会增加MySQL线程切换成本;connectionTimeout建议设置在3到10秒之间,避免线程无限等待;maxLifetime要小于MySQL的wait_timeout,防止连接被MySQL关闭后应用还在使用。
下面是一组适用于单实例多数据库场景的参考参数:
spring.datasource.hikari.pool-name=order-pool spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.connection-timeout=10000 spring.datasource.hikari.idle-timeout=600000 spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.leak-detection-threshold=30000
实际调优时,可以从默认值开始,观察连接池的等待曲线。如果经常出现等待超时,优先检查是否有慢查询或事务未及时提交,而不是盲目调大maximumPoolSize。同时,连接泄漏是单实例多库场景的高发问题,开启leak-detection-threshold后,连接被持有超过阈值时会打印警告日志,能帮助快速定位未归还连接的代码路径。
另外,线上发布或重启应用时,建议配置优雅关闭。让应用在停止前先释放数据库连接,避免MySQL堆积大量CLOSE_WAIT。对于使用Tomcat或Spring Boot的服务,可以注册JVM关闭钩子,调用HikariDataSource的close方法,确保连接有序归还。

