手写mybatis之数据源池化技术实现

news2024/10/8 20:27:51

前言

在上一章节我们解析了 XML 中数据源配置信息,并使用 Druid 创建数据源完成数据库的操作。但其实在 Mybatis 中是有自己的数据源实现的,包括无池化的 UnpooledDataSource 实现方式和有池化的 PooledDataSource 实现方式。

你可以把池化技术理解为享元模式的具体实现方案,通常我们对一些需要较高创建成本且高频使用的资源,需要进行缓存或者也称预热处理。并把这些资源存放到一个预热池子中,需要用的时候从池子中获取,使用完毕再进行使用。通过池化可以非常有效的控制资源的使用成本,包括;资源数量、空闲时长、获取方式等进行统一控制和管理。

在这里插入图片描述
此外由于控制了连接池中连接的数量,所以当外部从连接池获取链接时,如果链接已满则会进行循环等待。这也是大家日常使用DB连接池,如果一个SQL操作引起了慢查询,则会导致整个服务进入瘫痪的阶段,各个和数据库相关的接口调用,都不能获得到链接,接口查询TP99陡然增高,系统开始大量报警。那连接池可以配置的很大吗,也不可以,因为连接池要和数据库所分配的连接池对应上,避免应用配置连接池超过数据库所提供的连接池数量,否则会出现夯住不能分配链接的问题,导致数据库拖垮从而引起连锁反应。
无池化链接实现
对于数据库连接池的实现,不一定非得提供池化技术,对于某些场景可以只使用无池化的连接池。那么在实现的过程中,可以把无池化的实现和池化实现拆分解耦,在需要的时候只需要配置对应的数据源即可。

public class UnpooledDataSource implements DataSource {

    private ClassLoader driverClassLoader;
    // 驱动配置,也可以扩展属性信息 driver.encoding=UTF8
    private Properties driverProperties;
    // 驱动注册器
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<>();
    // 驱动
    private String driver;
    // DB 链接地址
    private String url;
    // 账号
    private String username;
    // 密码
    private String password;
    // 是否自动提交
    private Boolean autoCommit;
    // 事务级别
    private Integer defaultTransactionIsolationLevel;

    static {
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();
            registeredDrivers.put(driver.getClass().getName(), driver);
        }
    }

    private Connection doGetConnection(Properties properties) throws SQLException {
        initializerDriver();
        Connection connection = DriverManager.getConnection(url, properties);
        if (autoCommit != null && autoCommit != connection.getAutoCommit()) {
            connection.setAutoCommit(autoCommit);
        }
        if (defaultTransactionIsolationLevel != null) {
            connection.setTransactionIsolation(defaultTransactionIsolationLevel);
        }
        return connection;
    }

    /**
     * 初始化驱动
     */
    private synchronized void initializerDriver() throws SQLException {
        if (!registeredDrivers.containsKey(driver)) {
            try {
                Class<?> driverType = Class.forName(driver, true, driverClassLoader);
                // https://www.kfu.com/~nsayer/Java/dyn-jdbc.html
                Driver driverInstance = (Driver) driverType.newInstance();
                DriverManager.registerDriver(new DriverProxy(driverInstance));
                registeredDrivers.put(driver, driverInstance);
            } catch (Exception e) {
                throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
            }
        }
    }
	
}

1:无池化的数据源链接实现比较简单,核心在于 initializerDriver 初始化驱动中使用了 Class.forName 和 newInstance 的方式创建了数据源链接操作。
2:在创建完成连接以后,把链接存放到驱动注册器中,方便后续使用中可以直接获取链接,避免重复创建所带来的资源消耗。
有池化链接实现
由于我们需要对连接进行池化处理,所以当链接调用一些 CLOSE 方法的时候,也需要把链接从池中关闭和恢复可用,允许其他用户获取到链接。那么这里就需要对连接类进行代理包装,处理 CLOSE 方法。

public class PooledConnection implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        // 如果是调用 CLOSE 关闭链接方法,则将链接加入连接池中,并返回null
        if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
            dataSource.pushConnection(this);
            return null;
        } else {
            if (!Object.class.equals(method.getDeclaringClass())) {
                // 除了toString()方法,其他方法调用之前要检查connection是否还是合法的,不合法要抛出SQLException
                checkConnection();
            }
            // 其他方法交给connection去调用
            return method.invoke(realConnection, args);
        }
    }
    
}

1:在 invoke 方法中处理对 CLOSE 方法控制以外,排除 toString 等Object 的方法后,则是其他真正需要被 DB 链接处理的方法了。
2:那么这里有一个对于 CLOSE 方法的数据源回收操作 dataSource.pushConnection(this); 有一个具体的实现方法,在池化实现类 PooledDataSource 中进行处理。
pushConnection 回收链接

protected void pushConnection(PooledConnection connection) throws SQLException {
    synchronized (state) {
        state.activeConnections.remove(connection);
        // 判断链接是否有效
        if (connection.isValid()) {
            // 如果空闲链接小于设定数量,也就是太少时
            if (state.idleConnections.size() < poolMaximumIdleConnections && connection.getConnectionTypeCode() == expectedConnectionTypeCode) {
                state.accumulatedCheckoutTime += connection.getCheckoutTime();
                if (!connection.getRealConnection().getAutoCommit()) {
                    connection.getRealConnection().rollback();
                }
                // 实例化一个新的DB连接,加入到idle列表
                PooledConnection newConnection = new PooledConnection(connection.getRealConnection(), this);
                state.idleConnections.add(newConnection);
                newConnection.setCreatedTimestamp(connection.getCreatedTimestamp());
                newConnection.setLastUsedTimestamp(connection.getLastUsedTimestamp());
                connection.invalidate();
                logger.info("Returned connection " + newConnection.getRealHashCode() + " to pool.");
                // 通知其他线程可以来抢DB连接了
                state.notifyAll();
            }
            // 否则,空闲链接还比较充足
            else {
                state.accumulatedCheckoutTime += connection.getCheckoutTime();
                if (!connection.getRealConnection().getAutoCommit()) {
                    connection.getRealConnection().rollback();
                }
                // 将connection关闭
                connection.getRealConnection().close();
                logger.info("Closed connection " + connection.getRealHashCode() + ".");
                connection.invalidate();
            }
        } else {
            logger.info("A bad connection (" + connection.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
            state.badConnectionCount++;
        }
    }
}

popConnection 获取链接

private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;
    while (conn == null) {
        synchronized (state) {
            // 如果有空闲链接:返回第一个
            if (!state.idleConnections.isEmpty()) {
                conn = state.idleConnections.remove(0);
                logger.info("Checked out connection " + conn.getRealHashCode() + " from pool.");
            }
            // 如果无空闲链接:创建新的链接
            else {
                // 活跃连接数不足
                if (state.activeConnections.size() < poolMaximumActiveConnections) {
                    conn = new PooledConnection(dataSource.getConnection(), this);
                    logger.info("Created connection " + conn.getRealHashCode() + ".");
                }
                // 活跃连接数已满
                else {
                    // 取得活跃链接列表的第一个,也就是最老的一个连接
                    PooledConnection oldestActiveConnection = state.activeConnections.get(0);
                    long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
                    // 如果checkout时间过长,则这个链接标记为过期
                    if (longestCheckoutTime > poolMaximumCheckoutTime) {
                        state.claimedOverdueConnectionCount++;
                        state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                        state.accumulatedCheckoutTime += longestCheckoutTime;
                        state.activeConnections.remove(oldestActiveConnection);
                        if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                            oldestActiveConnection.getRealConnection().rollback();
                        }
                        // 删掉最老的链接,然后重新实例化一个新的链接
                        conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                        oldestActiveConnection.invalidate();
                        logger.info("Claimed overdue connection " + conn.getRealHashCode() + ".");
                    }
                    // 如果checkout超时时间不够长,则等待
                    else {
                        try {
                            if (!countedWait) {
                                state.hadToWaitCount++;
                                countedWait = true;
                            }
                            logger.info("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                            long wt = System.currentTimeMillis();
                            state.wait(poolTimeToWait);
                            state.accumulatedWaitTime += System.currentTimeMillis() - wt;
                        } catch (InterruptedException e) {
                            break;
                        }
                    }
                }
            }
            // 获得到链接
            if (conn != null) {
                if (conn.isValid()) {
                    if (!conn.getRealConnection().getAutoCommit()) {
                        conn.getRealConnection().rollback();
                    }
                    conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
                    // 记录checkout时间
                    conn.setCheckoutTimestamp(System.currentTimeMillis());
                    conn.setLastUsedTimestamp(System.currentTimeMillis());
                    state.activeConnections.add(conn);
                    state.requestCount++;
                    state.accumulatedRequestTime += System.currentTimeMillis() - t;
                } else {
                    logger.info("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
                    // 如果没拿到,统计信息:失败链接 +1
                    state.badConnectionCount++;
                    localBadConnectionCount++;
                    conn = null;
                    // 失败次数较多,抛异常
                    if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
                        logger.debug("PooledDataSource: Could not get a good connection to the database.");
                        throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                    }
                }
            }
        }
    }

    return conn;
}

数据源工厂
数据源工厂包括两部分,分别是无池化和有池化,有池化的工厂继承无池化工厂,因为在 Mybatis 源码的实现类中,这样就可以减少对 Properties 统一包装的反射方式的属性处理。由于我们暂时没有对这块逻辑进行开发,只是简单的获取属性传参,所以还不能体现出这样的继承有多便捷,读者可以参考源码进行理解。源码类:UnpooledDataSourceFactory。
无池化工厂

public class UnpooledDataSourceFactory implements DataSourceFactory {

    protected Properties props;

    @Override
    public void setProperties(Properties props) {
        this.props = props;
    }

    @Override
    public DataSource getDataSource() {
        UnpooledDataSource unpooledDataSource = new UnpooledDataSource();
        unpooledDataSource.setDriver(props.getProperty("driver"));
        unpooledDataSource.setUrl(props.getProperty("url"));
        unpooledDataSource.setUsername(props.getProperty("username"));
        unpooledDataSource.setPassword(props.getProperty("password"));
        return unpooledDataSource;
    }

}

有池化工厂

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

    @Override
    public DataSource getDataSource() {
        PooledDataSource pooledDataSource = new PooledDataSource();
        pooledDataSource.setDriver(props.getProperty("driver"));
        pooledDataSource.setUrl(props.getProperty("url"));
        pooledDataSource.setUsername(props.getProperty("username"));
        pooledDataSource.setPassword(props.getProperty("password"));
        return pooledDataSource;
    }

}

测试
配置数据源

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="DRUID">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>

配置Mapper

<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id}
</select>

单元测试

@Test
public void test01() throws IOException {
    // 1. 从SqlSessionFactory中获取SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    
    // 2. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    
    // 3. 测试验证
    for (int i = 0; i < 50; i++) {
        User user = userDao.queryUserInfoById(1L);
        logger.info("测试结果:{}", JSON.toJSONString(user));
    }
}

无池化测试

<dataSource type="UNPOOLED"></dataSource>

有池化测试

<dataSource type="POOLED"></dataSource>

总结
1:我们完成了 Mybatis 数据源池化的设计和实现,也能通过这样的分析、实现、验证的过程让大家更好的理解我们平常使用的连接池所遇到的一些真实问题都是怎么发生的,做到知其然知其所以然。
另外关于连接池的实现重点可以随着调试验证的过程中进行学习,包括:synchronized 加锁、创建连接、活跃数量控制、休眠等待时长,抛异常逻辑等,这些都与我们日常使用连接池时的配置息息相关。
2:这一章节的内容可以算作是 Mybatis 核心功能实现过程上的重要分支,虽然可以使用 Druid 替代数据源的处理,但只有动手自己实现一遍数据源连接池才能更好的理解池化技术的落地方案,也能为以后做此类功能时,有一个可落地的具体方案。

好了到这里就结束了手写mybatis之数据源池化技术实现的学习,大家一定要跟着动手操作起来。需要源码的 可si我获取;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2197174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何将精益思维应用于智能音箱的产品设计?

在激烈的市场竞争中&#xff0c;如何让自家的智能音箱脱颖而出&#xff0c;成为用户心中的“智能生活伴侣”&#xff1f;答案或许就藏在“精益思维”这一理念之中。本文&#xff0c;天行健精益生产顾问将带大家深入探索&#xff0c;如何将精益思维巧妙应用于智能音箱的产品设计…

排序算法之你不得不知道的(1)

一、排序运用场景 &#xff08;1&#xff09;排序概念&#xff1a;将一组杂乱无章的数据按照一定的规律&#xff08;升序或降序&#xff09;组织起来。 &#xff08;2&#xff09;运用场景&#xff1a; &#xff08;1&#xff09;淘宝、支付宝、京东等购物平台的价格排序、质…

vue的h函数和template语法如何混用?

前言&#xff1a; h函数定义&#xff1a; 我们编写的代码转化为真正的dom时&#xff0c;首先会先转换为VNode,然后多个Vnode进行结合起来转化为VDOM&#xff0c;最后VDOM才渲染成真实的DOM。在 Vue.js 中&#xff0c;h 函数是 createElement 的别名&#xff0c;它是 Vue 用来创…

如何使用ssm实现基于vue的学生宿舍设备报修管理系统的设计与实现+vue

TOC ssm804基于vue的学生宿舍设备报修管理系统的设计与实现vue 绪论 1.1 选题背景 当人们发现随着生产规模的不断扩大&#xff0c;人为计算方面才是一个巨大的短板&#xff0c;所以发明了各种计算设备&#xff0c;从结绳记事&#xff0c;到算筹&#xff0c;以及算盘&#x…

基于java SpringBoot和Vue校园求职招聘系统设计

摘要 随着信息技术的迅猛发展&#xff0c;基于Java Spring Boot和Vue的校园求职招聘系统设计成为了解决高校就业难问题的重要手段。本文旨在探讨如何利用Java Spring Boot框架构建后端服务&#xff0c;以及使用Vue.js进行前端开发&#xff0c;从而创建一个高效、易用且功能全面…

【JavaScript】JS核心语法及函数

文章目录 一、初识 JS二、JS 核心语法2-1 变量2-2 数据类型typeofString 对象 2-3 数组创建数组常用属性方法 2-4 运算符号加号运算符 减号运算符 -比较运算符逻辑运算符 2-5 控制语句for-inbreakcontinue 三、函数3-1 常用系统函数3-2 自定义函数函数声明函数调用 3-3 创建对象…

RISC-V知识点目录

分支预测 分支预测概述https://blog.csdn.net/zhangshangjie1/article/details/136947089?sharetypeblogdetail&sharerId136947089&sharereferPC&sharesourcezhangshangjie1&spm1011.2480.3001.8118分支指令的方向预测https://blog.csdn.net/zhangshangjie1/a…

高效微调理解(prompt-tuning,p-tuning v1,p-tuning v2,lora)

高效微调&#xff08;prompt-tuning&#xff0c;p-tuning v1&#xff0c;p-tuning v2&#xff0c;lora&#xff09; 1.prompt-tuning&#xff1a; 例子理解&#xff1b;保持原本模型参数不变&#xff0c;通过训练提示词的参数调整prompt&#xff0c;使其与下游任务匹配。 例子…

HCIP-HarmonyOS Application Developer 习题(八)

&#xff08;填空&#xff09;1、声明式开发范式中使用装饰器( )装饰的结构体具有组件化能力&#xff0c;能够成为一个自定义组件。 答案&#xff1a;component 分析&#xff1a;component 装饰的struct表示该结构体具有组件化能力&#xff0c;能够成为一个独立的组件&#xff…

IT行业哪些证书可以应对就业难?

作为IT运维专业人士&#xff0c;持续增强自身的专业技能和知识是提升职场竞争力、实现升职加薪的关键途径。 下面为大家搜罗了5本适合IT运维人员考取的证书。 一、ITSS认证 ITSS&#xff0c;即信息技术服务标准&#xff0c;是一套涵盖了IT服务领域的标准库和方法论。 这是我…

企业升级首选:Windows 11 24H2 LTSC 纯净企业版!

今日&#xff0c;系统之家小编给大家带来最新的Windows11 24H2 LTSC 2024 纯净企业版下载&#xff0c;该版本系统是离线制作而成&#xff0c;各种各样的捆绑软件也都删除了&#xff0c;确保系统是安全无毒&#xff0c;还具备出色的稳定性与安全性&#xff0c;非常适合企业用户办…

自动猫砂盆真的有必要入手吗?自用不踩雷的选购干货分享!

平时出门在外忙碌&#xff0c;要如何保持猫咪的猫砂盆卫生就成了一个很重要的问题&#xff0c;要知道猫咪拉屎需求特别频繁&#xff0c;如果猫砂盆里的猫屎堆积过量&#xff0c;猫咪就很有可能嫌弃&#xff0c;然后寻找其他地方排泄&#xff0c;这就导致了家里大大小小都充斥着…

Python精选200Tips:186-190

针对序列&#xff08;时间、文本&#xff09;数据的网络结构 续 P186-- 双向LSTM(Bidirectional Long Short-Term Memory 2005)&#xff08;1&#xff09;模型结构说明&#xff08;2&#xff09;创新性说明&#xff08;3&#xff09;示例代码&#xff1a;IMDB电影评论情感分析 …

python爬虫 - 进阶requests模块

&#x1f308;个人主页&#xff1a;https://blog.csdn.net/2401_86688088?typeblog &#x1f525; 系列专栏&#xff1a;https://blog.csdn.net/2401_86688088/category_12797772.html 目录 前言 一、SSL证书问题 &#xff08;一&#xff09;跳过 SSL 证书验证 &#xff0…

【C++打怪之路Lv7】-- 模板初阶

&#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;C打怪之路&#xff0c;python从入门到精通&#xff0c;数据结构&#xff0c;C语言&#xff0c;C语言题集&#x1f448; 希望得到您的订阅和支持~ &#x1f4a1; 坚持创作博文(平均质量分82)&#…

基于SpringBoot的设备管理系统源码带本地搭建教程

技术框架&#xff1a;SpringBoot mybatis thymeleaf Mysql5.7 Fastjson Druid Shiro 运行环境&#xff1a;jdk8 IntelliJ IDEA maven 宝塔面板 系统功能&#xff1a;登陆&#xff0c;注册&#xff0c;系统用户管理&#xff0c;角色&#xff0c;部门管理&#xff0c;…

如何在电脑上创建虚拟网卡

1.右键点击此电脑&#xff0c;选择——管理 2.选择设备管理器——网络适配器&#xff0c;在点击操作选择 添加过时硬件 3.点击 下一页 4.在这里选择网络适配器&#xff0c;点击下一页 5.选择微软的环回适配器 6.打开控制面板 7.点击网络和Internet 8.点击网络和共享中心 9…

多表数据实时同步和批量实时同步怎么高效实现?

对于企业来说&#xff0c;准确、及时的数据是进行数据分析和决策支持的基础。如果各个系统中的数据不能及时同步&#xff0c;就会影响数据分析的结果和决策的准确性。通过数据同步&#xff0c;可以将企业内部各个系统中的数据整合到一个数据仓库或数据分析平台中&#xff0c;为…

Jenkins打包,发布,部署

一、概念 Jenkins是一个开源的持续集成工具&#xff0c;主要用于自动构建和测试软件项目&#xff0c;以及监控外部任务的运行。与版本管理工具&#xff08;如SVN&#xff0c;GIT&#xff09;和构建工具&#xff08;如Maven&#xff0c;Ant&#xff0c;Gradle&#xff09;结合使…

【LeetCode刷题】:双指针篇(移动零、复写零)

文章目录 一、移动零1. 题目解析2. 算法原理3. 代码编写 二、复写零1. 题目解析2. 算法原理3. 代码编写 一、移动零 1. 题目解析 题目&#xff1a;移动零【点击跳转题目】 大致题意就是将数组中所有为0的元素往后移&#xff0c;移到数组的末尾&#xff0c;但是所有的非零元素…