dataSource
导学
本节学习目标:
- 理解
dataSource是 MyBatis 获取数据库连接的"源头" - 掌握三种内置数据源类型
UNPOOLED、POOLED、JNDI的行为差异 - 深入理解
POOLED连接池的核心参数与工作原理 - 能够根据项目场景选择合适的数据源类型并合理调优
定义
数据库连接是昂贵资源:建立一次 TCP 连接、完成 MySQL 握手认证、分配线程和内存,整个过程耗时数十到数百毫秒。如果每次执行 SQL 都新建连接,应用性能将急剧下降,数据库也会因频繁创建连接而负载飙升。
dataSource 元素的作用就是配置 MyBatis 如何获取和管理数据库连接。MyBatis 内置了三种数据源实现,覆盖从开发调试到生产部署的全场景需求。
适用位置与核心属性
dataSource 位于 mybatis-config.xml 中每个 environment 内部,且必须是 transactionManager 的下一个子元素。
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="poolMaximumActiveConnections" value="20"/>
<property name="poolMaximumIdleConnections" value="10"/>
</dataSource>
</environment>
| 属性 | 是否必填 | 说明 |
|---|---|---|
type | 必填 | 数据源类型。内置支持 UNPOOLED、POOLED、JNDI,也可指定自定义数据源类的全限定名 |
三种内置类型对比
| 类型 | 连接行为 | 适用场景 |
|---|---|---|
UNPOOLED | 每次请求都新建连接,用完立即关闭 | 开发调试、单元测试、极轻量级应用 |
POOLED | 维护连接池,复用已有连接 | 生产环境、高并发应用(强烈推荐) |
JNDI | 从容器(如 Tomcat、WebLogic)的 JNDI 树中获取 DataSource | 企业级 Java EE 应用、容器统一管理数据源 |
POOLED 核心参数
| 属性名 | 默认值 | 说明 |
|---|---|---|
poolMaximumActiveConnections | 10 | 连接池中最大活跃连接数。同时正在使用的连接数不能超过此值 |
poolMaximumIdleConnections | 5 | 连接池中最大空闲连接数。空闲连接超过此值时会被回收 |
poolMaximumCheckoutTime | 20000 | 连接被取出后最大允许使用时间(毫秒)。超过此时间未归还,连接被强制回收 |
poolTimeToWait | 20000 | 当连接池无可用连接时,线程等待的最大时间(毫秒) |
poolMaximumLocalBadConnectionTolerance | 3 | 容忍的坏连接次数。获取到无效连接时的重试次数 |
poolPingQuery | NO PING QUERY SET | 检测连接是否有效的测试 SQL,如 SELECT 1 |
poolPingEnabled | false | 是否开启连接健康检测 |
poolPingConnectionsNotUsedFor | 0 | 连接空闲超过此时间(毫秒)才执行检测。0 表示每次获取都检测 |
核心原理
连接池获取/归还连接时序图
源码层面:PooledDataSource 内部维护两个 ArrayList:
activeConnections:当前被取出的活跃连接(PooledConnection代理)idleConnections:当前空闲的连接
当应用调用 connection.close() 时,PooledConnection 的代理逻辑会拦截该方法,将连接归还到 idleConnections 而非真正关闭物理连接。
完整示例
场景说明
乐途公司员工管理系统部署在生产环境,预计峰值并发约 50 个数据库操作线程。需要配置 POOLED 数据源,确保:
- 最大活跃连接数能支撑峰值并发
- 空闲连接不过度占用数据库资源
- 连接异常时能自动检测和回收
操作前的状态
未使用连接池时(UNPOOLED),每次查询都经历完整的 TCP 连接建立过程:
获取连接耗时: 45ms
执行 SQL 耗时: 5ms
关闭连接耗时: 3ms
完整配置代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.feixiang.entity"/>
</typeAliases>
<environments default="production">
<environment id="production">
<transactionManager type="JDBC"/>
<!-- POOLED 连接池配置(生产环境推荐) -->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<!-- 连接池核心参数 -->
<property name="poolMaximumActiveConnections" value="50"/>
<property name="poolMaximumIdleConnections" value="20"/>
<property name="poolMaximumCheckoutTime" value="30000"/>
<property name="poolTimeToWait" value="20000"/>
<!-- 连接健康检测 -->
<property name="poolPingEnabled" value="true"/>
<property name="poolPingQuery" value="SELECT 1"/>
<property name="poolPingConnectionsNotUsedFor" value="600000"/>
</dataSource>
</environment>
<environment id="development">
<transactionManager type="JDBC"/>
<!-- 开发环境使用 UNPOOLED,简化调试 -->
<dataSource type="UNPOOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/EmployeeMapper.xml"/>
</mappers>
</configuration>
Java 性能对比测试代码:
public class DataSourceDemo {
public static void main(String[] args) throws Exception {
// 测试 POOLED 模式
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory pooledFactory = new SqlSessionFactoryBuilder().build(is, "production");
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
try (SqlSession session = pooledFactory.openSession()) {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
mapper.selectById(1);
} // 连接归还连接池
}
long pooledCost = System.currentTimeMillis() - start;
System.out.println("POOLED 模式 100 次查询总耗时: " + pooledCost + "ms");
// 测试 UNPOOLED 模式
is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory unpooledFactory = new SqlSessionFactoryBuilder().build(is, "development");
start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
try (SqlSession session = unpooledFactory.openSession()) {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
mapper.selectById(1);
} // 连接真正关闭
}
long unpooledCost = System.currentTimeMillis() - start;
System.out.println("UNPOOLED 模式 100 次查询总耗时: " + unpooledCost + "ms");
}
}
实际效果/结果
控制台输出(典型值):
POOLED 模式 100 次查询总耗时: 520ms
UNPOOLED 模式 100 次查询总耗时: 4850ms
效果验证:
POOLED模式首次获取连接需要建立物理连接(约 40ms),后续 99 次直接从连接池取出(约 1-2ms)UNPOOLED模式每次都要新建和关闭连接,总耗时约为POOLED的 9 倍- 开启
poolPingEnabled后,空闲超过 10 分钟的连接在取出时会执行SELECT 1验证有效性,避免使用已被数据库端关闭的"僵尸连接"
分析
poolMaximumActiveConnections不应设置过大,否则数据库端连接数耗尽,影响其他应用。一般设置为(峰值并发线程数 × 单线程最大同时连接需求) + 缓冲值poolMaximumIdleConnections建议设置为poolMaximumActiveConnections的 30%-50%,既能应对突发流量,又不会长期占用过多数据库连接poolMaximumCheckoutTime是防止连接泄漏的"保险丝":如果应用取出连接后忘记归还(如异常未正确关闭SqlSession),超过 30 秒会被强制回收poolPingConnectionsNotUsedFor不建议设为 0(每次获取都检测),会增加一次网络往返。生产环境建议设为 5-10 分钟
易错场景/常见误区
| 误区 | 错误表现 | 正解 |
|---|---|---|
生产环境使用 UNPOOLED | 应用响应慢,数据库连接数波动剧烈,频繁出现 Too many connections | 生产环境必须使用 POOLED,这是最基本的数据库性能优化 |
poolMaximumActiveConnections 设置过大 | 数据库端连接数被打满,导致其他应用或备份任务无法连接 | 根据数据库最大连接数和实际并发评估,通常不超过 50-100 |
poolMaximumActiveConnections 设置过小 | 高并发时线程频繁等待,超时抛出异常 | 通过压测确定合理值,公式:峰值并发 × 1.2 |
| 未开启连接健康检测 | 应用运行一段时间后出现 Communications link failure,查询失败 | 生产环境开启 poolPingEnabled,配置轻量检测 SQL(如 SELECT 1) |
认为 poolMaximumCheckoutTime 是连接最大存活时间 | 连接被频繁强制关闭,性能下降 | poolMaximumCheckoutTime 是"单次取出最大使用时间",不是连接总寿命。连接总寿命由数据库的 wait_timeout 控制 |
JNDI 类型在独立应用中使用 | 启动报 NameNotFoundException | JNDI 只能在 Java EE 容器(Tomcat、WebLogic 等)中使用,独立应用应使用 POOLED |
面试考点
Q1:MyBatis 的 POOLED 数据源和 UNPOOLED 数据源有什么区别?
A:
UNPOOLED每次请求都新建物理连接,用完立即关闭,适合开发和测试,但生产环境性能极差。POOLED维护一个连接池,复用已有连接,显著减少连接建立开销,是生产环境的标准选择。POOLED内部通过PooledConnection动态代理拦截close()方法,实现连接的归还而非真正关闭。
Q2:POOLED 连接池的核心参数有哪些?如何调优?
A:核心参数包括:
poolMaximumActiveConnections:最大活跃连接数,根据峰值并发和数据库容量设置poolMaximumIdleConnections:最大空闲连接数,通常为活跃数的 30%-50%poolMaximumCheckoutTime:连接最大取出时间,防止连接泄漏poolTimeToWait:无可用连接时的等待时间poolPingEnabled+poolPingQuery:连接健康检测,避免使用失效连接 调优原则:通过压测确定活跃连接数,开启健康检测,设置合理的超时时间防止泄漏。
Q3:为什么 POOLED 模式下调用 connection.close() 不会真正关闭连接?
A:因为 MyBatis 返回给应用的是
PooledConnection动态代理对象。当应用调用close()时,代理对象的invoke()方法会拦截该调用,将底层物理连接从activeConnections移回idleConnections,而非调用真实连接的close()。这是连接池复用连接的核心机制。
Q4:MyBatis 内置连接池与 HikariCP、Druid 等第三方连接池相比如何?
A:MyBatis 内置的
POOLED是一个轻量级连接池,功能相对简单,适合不引入额外依赖的场景。但生产环境中,HikariCP(性能极致)、Druid(监控完善)等专用连接池在性能、监控、防护(SQL 注入检测、连接泄漏回收)方面更强大。Spring Boot 项目通常将dataSource类型设为UNPOOLED或JNDI,由 Spring 注入 HikariCP 等数据源。
小结
dataSource 是 MyBatis 与数据库之间的"桥梁",其配置直接决定了应用的性能基线。UNPOOLED 仅用于开发和测试;POOLED 是生产环境的唯一选择,合理调优连接池参数能显著提升吞吐量并防止连接泄漏;JNDI 则服务于企业级容器环境。记住:连接池不是越大越好,匹配实际并发、开启健康检测、设置超时保护,才是调优的正道。
下一章引子
连接池解决了单数据库的性能问题,但现代企业应用常常需要适配多种数据库(如 Oracle 用于核心账务、MySQL 用于业务系统、PostgreSQL 用于数据分析)。MyBatis 的 databaseIdProvider 提供了多数据库适配机制,允许同一套 Mapper 根据当前数据库类型执行不同的 SQL 方言。接下来,我们将探索这一高级特性。