手写MyBatis ORM框架(五)

手写MyBatis ORM框架(五)

引言

上一部分我们实现了解析XML文件中的数据源配置信息,这篇文章我们来实现数据源。

开始

MyBatis源码中自带的数据源类型

MyBatis 自带了三种内置的数据源(DataSource)类型,通过配置文件中的 <dataSource> 标签的 type 属性指定。以下是它们的详细说明及使用场景:


1. **UNPOOLED**(非池化数据源)

特点

  • 每次请求新建连接:每次执行 SQL 时创建新的数据库连接,用完后直接关闭。

  • 无连接池管理:适用于轻量级或测试场景,不适合高并发生产环境。

  • 简单高效:没有连接池的开销,但频繁创建/销毁连接性能较差。

配置示例

<dataSource type="UNPOOLED">
  <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://localhost:3306/test"/>
  <property name="username" value="root"/>
  <property name="password" value="123456"/>
  <!-- 可选参数 -->
  <property name="autoCommit" value="false"/> <!-- 是否自动提交事务 -->
  <property name="defaultTransactionIsolationLevel" value="READ_COMMITTED"/> <!-- 事务隔离级别 -->
</dataSource>

适用场景

  • 本地开发或单元测试。

  • 对性能要求不高的简单应用。


2. **POOLED**(池化数据源)

特点

  • 连接池管理:复用数据库连接,减少频繁创建/销毁连接的开销。

  • 性能优化:适合生产环境,支持并发请求。

  • 自动回收:空闲连接超时或活动连接超过最大数量时,自动关闭冗余连接。

配置示例

<dataSource type="POOLED">
  <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
  <property name="url" value="jdbc:mysql://localhost:3306/test"/>
  <property name="username" value="root"/>
  <property name="password" value="123456"/>
  <!-- 连接池参数 -->
  <property name="poolMaximumActiveConnections" value="10"/> <!-- 最大活动连接数 -->
  <property name="poolMaximumIdleConnections" value="5"/>  <!-- 最大空闲连接数 -->
  <property name="poolMaximumCheckoutTime" value="20000"/> <!-- 连接最大借用时间(毫秒) -->
  <property name="poolTimeToWait" value="20000"/>          <!-- 获取连接超时时间(毫秒) -->
</dataSource>

核心参数说明

参数

默认值

说明

poolMaximumActiveConnections

10

连接池中允许的最大活动连接数。

poolMaximumIdleConnections

5

连接池中允许的最大空闲连接数。

poolMaximumCheckoutTime

20000

连接被借用后最长使用时间(超时未归还则强制回收)。

poolTimeToWait

20000

获取连接时等待的超时时间(若没有可用连接)。

poolPingEnabled

false

是否启用心跳检测(定期检查连接有效性)。

poolPingQuery

心跳检测时执行的 SQL(如 SELECT 1)。

适用场景

  • 生产环境中的中小型应用。

  • 需要平衡性能和资源消耗的场景。


3. **JNDI**(容器管理的数据源)

特点

  • 从容器获取连接:数据源由 Java EE 容器(如 Tomcat、WebLogic)管理,MyBatis 通过 JNDI 名称查找。

  • 集中配置:数据源的连接池参数在容器中统一配置,与 MyBatis 解耦。

  • 适合企业级应用:通常用于需要与容器集成的场景。

配置示例

<dataSource type="JNDI">
  <property name="initial_context" value="java:comp/env"/> <!-- JNDI 初始上下文 -->
  <property name="data_source" value="jdbc/MyDB"/>        <!-- 数据源的 JNDI 名称 -->
</dataSource>

容器配置示例(Tomcat)

在 Tomcat 的 context.xml 中配置数据源:

<Context>
  <Resource 
    name="jdbc/MyDB"
    auth="Container"
    type="javax.sql.DataSource"
    driverClassName="com.mysql.cj.jdbc.Driver"
    url="jdbc:mysql://localhost:3306/test"
    username="root"
    password="123456"
    maxTotal="20"          <!-- 最大连接数 -->
    maxIdle="10"           <!-- 最大空闲连接数 -->
    maxWaitMillis="10000"  <!-- 获取连接超时时间 -->
  />
</Context>

适用场景

  • Java EE 环境(如 Tomcat、JBoss 等容器)。

  • 需要与容器共享数据源的分布式应用。


4. 内置数据源的实现类

MyBatis 内置数据源的底层实现类如下:

  • UNPOOLEDorg.apache.ibatis.datasource.unpooled.UnpooledDataSource

  • POOLEDorg.apache.ibatis.datasource.pooled.PooledDataSource

  • JNDIorg.apache.ibatis.datasource.jndi.JndiDataSource


5. 如何选择数据源?

数据源类型

推荐场景

UNPOOLED

开发测试、单线程应用、不需要连接池的场景。

POOLED

生产环境、中小型并发应用、希望避免频繁创建连接的开销。

JNDI

企业级应用、数据源由容器管理、需与其他组件(如 JPA、EJB)共享连接池的场景。


6. 对比第三方连接池(如 HikariCP、Druid)

对比项

MyBatis 内置 POOLED

第三方连接池(如 HikariCP)

性能

中等(适合中小规模应用)

更高(优化了连接池算法)

功能

基础功能(连接复用、超时控制)

高级功能(监控、SQL 防火墙、防御注入)

配置复杂度

简单(直接通过 MyBatis 配置)

需额外依赖和配置

适用场景

快速集成、无需额外依赖

高并发、需要监控和扩展功能的生产环境


非池化数据源实现

MyBatis 的无池化数据源(Unpooled DataSource)实现机制相对简单,核心在于每次请求时直接创建新的数据库连接,使用后立即关闭。

  • 无池化的数据源链接实现比较简单,核心在于 initializerDriver 初始化驱动中使用了 Class.forName 和 newInstance 的方式创建了数据源链接操作。

  • 在创建完成连接以后,把连接存放到驱动注册器中,方便后续使用中可以直接获取连接,避免重复创建所带来的资源消耗。

池化数据源实现

池化的数据源连接,核心在于对无池化连接的包装,同时提供了相应的池化技术实现,包括:pushConnection、popConnection、forceCloseAll、pingConnection 的操作处理。

这样当用户想要获取连接的时候,则会从连接池中进行获取,同时判断是否有空闲连接、最大活跃连接多少,以及是否需要等待处理或是最终抛出异常。

池化连接的代理

由于我们需要对连接进行池化处理,所以当链接调用一些 CLOSE 方法的时候,也需要把链接从池中关闭和恢复可用,允许其他用户获取到链接。那么这里就需要对连接类进行代理包装,处理 CLOSE 方法。

  • 通过 PooledConnection 实现 InvocationHandler#invoke 方法,包装代理链接,这样就可以对具体的调用方法进行控制了。

  • 在 invoke 方法中处理对 CLOSE 方法控制以外,排除 toString 等Object 的方法后,则是其他真正需要被 DB 链接处理的方法了。

  • 那么这里有一个对于 CLOSE 方法的数据源回收操作 dataSource.pushConnection(this); 有一个具体的实现方法,在池化实现类 PooledDataSource 中进行处理。

方法实现

1. pushConnection

作用:将使用完毕的连接归还到空闲连接池(idleConnections),供后续复用。 流程

  1. 重置连接状态(如回滚未提交的事务、恢复自动提交模式)。

  2. 检查空闲池是否已满:

    1. 若未满,将连接加入空闲池,并记录 lastUsedTime(最后使用时间)。

    2. 若已满,直接关闭物理连接。

  3. 从活跃池(activeConnections)中移除该连接。

  4. 通知等待线程(通过 notifyAll),唤醒可能阻塞在 popConnection 中的其他线程。


2. popConnection

作用:从连接池中获取一个可用连接。 流程

  1. 循环尝试获取连接

    1. 检查空闲池是否有可用连接:

      • 若有,取出第一个空闲连接,验证有效性(通过 pingConnection)。

      • 若无效,关闭该连接并重试。

    2. 若空闲池无连接,检查活跃池是否已满:

      • 若未满,创建新连接并加入活跃池。

      • 若已满,线程等待(wait(poolTimeToWait))直到超时或有连接被归还。

  2. 将连接标记为活跃状态,记录 checkoutTime(取出时间)。

  3. 返回代理连接(ProxyConnection),拦截 close() 方法以实现归还逻辑。


3. forceCloseAll

作用:强制关闭所有空闲和活跃连接,通常用于连接池销毁或紧急重置。 流程

  1. 遍历空闲池和活跃池,逐个调用 realClose() 关闭物理连接。

  2. 清空空闲池和活跃池。

  3. 重置连接池状态(如计数器)。


4. pingConnection

作用:验证连接是否有效,防止返回已失效的连接(如数据库超时断开)。 流程

  1. 发送测试查询(如 SELECT 1)或调用 Connection.isValid(timeout)

  2. 若测试成功,返回 true;否则返回 false

  3. 若连接无效,调用 realClose() 关闭物理连接。

池状态

线程安全控制 通过 synchronized(state) 对连接池的关键操作(如获取连接、归还连接)加锁,防止多线程竞争导致数据不一致。

资源状态管理 维护连接池的实时状态,包括:

  • 空闲连接列表(idleConnections

  • 活跃连接列表(activeConnections

  • 当前等待连接的线程数

  • 统计信息(如总连接数、超时次数等)

线程间通信 使用 state.wait()state.notifyAll() 实现阻塞唤醒机制:

  • 当线程无法获取连接时,调用 state.wait() 进入等待。

  • 当连接被归还时,调用 state.notifyAll() 唤醒等待线程。

流程图

无池化工厂

简单包装 getDataSource 获取数据源处理,把必要的参数进行传递过去。

池化工厂

池化的数据源工厂实现的也比较简单,只是继承 UnpooledDataSourceFactory 共用获取属性的能力,以及实例化出池化数据源即可。

LICENSED UNDER CC BY-NC-SA 4.0