从原理到实践,讲透 MyBatis 内部池化思想的核心逻辑

前言

在之前的文章中,我们提及到通过Trae的编码能力实现复刻MyBtatis的思路,如今,我们相关的mini-mybatis已逐渐成型,感兴趣的小伙伴可访问如下地址:mini-mybatis 进行学习 。其中,每个模块所对应知识点如下:

本次我们主要深入剖析Mybatis中内部池化思想的设计实践,相关代码可参考模块mini-mybatis-datasource

池化思想

事实上,我们平时所谈及的 池化(pool) 其底层逻辑都是一样的。即提前准备好资源,需要使用时直接获取,用完再放回,进而避免对象反复创建、销毁的资源损耗

以生活中最常见的共享单车为例,平台会在城市各处的停靠点预先部署自行车,用户通过扫码使用,待到达目的地后再将车辆停回指定区域,进而方便其他用户继续使用。

当然,未按规则停放在服务范围内,则需缴纳额外费用。这其实是通过规则约束资源使用行为,避免资源滥用或流失。

将这一逻辑过渡到我们所熟悉的SQL查询执行中,对于无池化SQL操作,其与数据库通信逻辑如下:

  1. 首先,上层应用会调用 JDBC 驱动发起数据库请求
  2. 然后,请求与数据库通过三次握手建立 TCP连接;
  3. 其次,连接发送 SQL 执行指令,等待数据库返回结果;
  4. 最终,关闭 TCP 连接,进行四次挥手操作。

可能你会想,如果这一过程中的 "等待、沟通、结算" 耗时不长,那是不是并不会产生负面影响?

如果并发量不高情况下,理论上不进行池化操作也是可以的,但当业务高频次调用时,连接频繁的构建的 累计耗时,则会严重挤占业务逻辑时间。

比如,一次 SQL 查询本身仅需 10ms,而建立连接的过程可能就要 50ms,这意味着 80% 的时间都消耗在非业务相关的资源准备上, 进而降低整个业务的响应时间。

因此,池化出现的核心是在缓解 "资源创建 / 销毁成本高" 与 "资源需求频繁" 之间的矛盾。

通常来看, 实现池化操作通常会进行如下几步:

  1. 资源预创建:在系统初始化时,提前创建一批可用的资源,存到一个容器中,进而避免用的时候临时创建;
  2. 资源复用:业务需要资源时,直接从容器中获取,不用自己新建;待用完后不进行销毁,而是 "归还" 到池里,供其他业务复用;
  3. 资源管控:池会监控资源的状态(比如是否空闲、是否过期),还能配置 "最大资源数"(避免资源过多占满内存)、"最小空闲数"(保证随时有可用资源)、"超时回收"(清理长期不用的资源)。

Mybatis中的池化设计

在介绍Mybatis中池化思想时,我们先明确一个关键点。即 MyBatis 本身并不直接实现 "连接池",而是集成第三方连接池,并通过统一的 DataSource 接口封装池化逻辑

MyBatis 在有关数据源实现中,核心分为无池化的UnpooledDataSource与有池化的PooledDataSource。对于PooledDataSource 来说,其通过对数据库连接connection装饰,使得其close方法不再直接关闭连接,而是将连接存入内存,并且用两个集合分别管理 "空闲连接" 与 "活跃连接",供不同阶段业务复用,避免重复创建损耗

进一步来看,PooledConnection是连接的代理类,核心通过invoke方法反射拦截close操作:业务调用close时,不会真关闭 TCP 连接,而是将连接重置后归还空闲集合,同时调用notifyAll唤醒等待连接的线程,让线程竞争复用,减少阻塞。整个逻辑如下所示:

Mybatis相关池化代码剖析

在熟悉了Mybatis中实现池化的相关组件后,接下来,我们将Mybatis中实现池化的关键逻辑我们拆成 个关键流程,结合代码逻辑理解来进一步理解Mybatis内部池化逻辑实现的具体细节。

获取连接------ getConnection从连接池中获取

MyBatis 在执行 sqlSession.selectList(...) 时,其本质借助Environment对象中所维护的数据源来获取连接,即调用 dataSource.getConnection()。此处获取连接的代码调用逻辑大致如下:

PooledDataSource

java 复制代码
public Connection getConnection() throws SQLException {
 //覆盖了DataSource.getConnection方法,每次都是pop一个Connection,即从池中取出一个来
  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}

PooledDataSource # popConnection

java 复制代码
private PooledConnection popConnection(String username, String password) throws SQLException {
    // .... 省略无关代码
    while (conn == null) {
        // 资源锁住,避免并发问题
        synchronized (state) {
        // 1. 连接池中有空闲连接
            if (!state.idleConnections.isEmpty()) {
                 // .... 省略无关代码
            } else {
            // 2. 判断活跃连接数未超出限制
                if (state.activeConnections.size() < poolMaximumActiveConnections) {
                      // .... 省略无关代码
                } else {
              // 3. 最大连接数以超过限制,则获取通过LRU算法,
              // 获取最老连接判断其是否超时,
                 
                 // .... 省略无关代码

           }
      }
     // .... 省略无关代码
    return conn;
}

为了便于理解,我们对PooledConnection 的源码进行了精简。其中的if-else判断逻辑如下图所示:

总的来看,此处getConnection逻辑可简化如下两步:

  1. 先看 "空闲池" 有没有可用连接:如果有,直接取最前面的(FIFO 队列),移到 "活跃池"(activeConnections);

  2. 如果空闲池为空,检查 "活跃池数量" 是否超过 poolMaximumActiveConnections(最大活跃连接数):

    • 没超过:新建一个物理连接,加入活跃池;
    • 超过了:等待(poolMaximumWait),直到有连接归还,或超时抛出 "连接超时" 异常。

归还连接--- close方法的代理增强

如果我们未对connection进行缓存,当连接用完后会自动进行关闭。通过我们之前对池化的介绍和分析,当引入池化操作后,我们的会对资源进行一次缓存,而不是直接将其关闭。

具体到Mybatis内部,我们对于连接的关闭其实是将其归还至**放回空闲池这个队列中。 具体逻辑如下:

PooledDataSource # pushConnection

java 复制代码
protected void pushConnection(PooledConnection conn) throws SQLException {
    synchronized (state) {
        state.activeConnections.remove(conn);
        if (conn.isValid()) {
            // 未达到空闲最大连接数
            if (state.idleConnections.size() < poolMaximumIdleConnections) {
                state.accumulatedCheckoutTime += conn.getCheckoutTime();
                if (!conn.getRealConnection().getAutoCommit()) {
                    conn.getRealConnection().rollback();
                }
                PooledConnection newConnection = new PooledConnection(conn.getRealConnection(),this);
                newConnection.setCreatedTimestamp(conn.getCreatedTimestamp());
                newConnection.setLastUsedTimestamp(System.currentTimeMillis());
                state.notifyAll();
            } else {
                //反之,即空闲的连接已经足够了
                state.accumulatedCheckoutTime += conn.getCheckoutTime();
                if (!conn.getRealConnection().getAutoCommit()) {
                    conn.getRealConnection().rollback();
                }
                conn.getRealConnection().close();
                conn.invalidate();
            }
        }else {
            log.info("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
            state.badConnectionCount ++ ;
        }
    }
}

此处代码逻辑相对简单,主要考虑两种情况,如果空闲队列仍空闲空间可以使用,则将连接存在之队列的末尾,并唤醒之前已经等待的进行重新进行连接的获取;反之,如果空闲队列已经超过最大限制,则才将连接进行关闭。

总结

总的来看,MyBatis 内部连接池实现连接复用的本质,是通过 PooledConnection 代理改写 close () 方法+PooledDataSource 双池管控流程 ,从而将 "物理连接的创建 / 关闭" 转化为 "池内连接的复用 / 流转"。

这种设计既遵循 JDBC 规范(基于 DataSourceConnection 接口),又通过轻量级封装实现池化能力,是 "规范兼容" 与 "性能优化" 的平衡典范,也为集成第三方连接池(如 HikariCP、Druid)预留了扩展空间(通过实现 DataSource 接口即可替换)。

相关推荐
胡gh1 小时前
依旧性能优化,如何在浅比较上做文章,memo 满天飞,谁在裸奔?
前端·react.js·面试
在未来等你1 小时前
Redis面试精讲 Day 27:Redis 7.0/8.0新特性深度解析
数据库·redis·缓存·面试
胡gh1 小时前
你一般用哪些状态管理库?别担心,Zustand和Redux就能说个10分钟
前端·面试·node.js
小厂永远得不到的男人2 小时前
基于 Spring Validation 实现全局参数校验异常处理
java·后端·架构
bobz9655 小时前
复姓人口比例不到 0.11%
面试
uhakadotcom6 小时前
什么是esp32?
面试·架构·github
展信佳_daydayup6 小时前
02 基础篇-OpenHarmony 的编译工具
后端·面试·编译器
Always_Passion6 小时前
二、开发一个简单的MCP Server
后端
用户721522078776 小时前
基于LD_PRELOAD的命令行参数安全混淆技术
后端