《手写Mybatis渐进式源码实践》实践笔记(第六章 数据源池化技术的实现)



第6章 数据源池化技术的实现

mybatis

背景

技术背景

享元模式

享元模式(Flyweight Pattern)是一种结构型设计模式,其核心思想是通过共享技术有效地支持大量细粒度对象的复用,从而减少内存占用并提高性能。享元模式主要解决的问题是避免因创建大量对象而导致的内存溢出问题,通过共享对象提高内存使用效率。

享元模式的主要特点包括:
  1. 减少对象数量:享元模式通过重用现有的同类对象来减少创建对象的数量。
  2. 内部状态与外部状态分离:享元模式区分了对象的内部状态(Intrinsic State)和外部状态(Extrinsic State)。内部状态是共享的,而外部状态是对象特定的,会随着外部环境的变化而变化。
  3. 享元工厂:享元模式通常包含一个享元工厂(Flyweight Factory),用于创建和管理享元对象,确保享元对象的共享。
享元模式的应用场景:
  1. 系统中存在大量相似或相同的对象:当系统中有大量相似对象,且这些对象可以被共享时,使用享元模式可以减少内存占用。
  2. 对象的创建和销毁成本较高:如果对象的创建和销毁需要消耗大量资源,享元模式可以通过共享对象来减少这些开销。
  3. 对象状态可以外部化:如果对象的部分状态可以独立于对象本身存在,那么这些状态可以作为外部状态,而对象的共享部分则作为内部状态。
享元模式的优点:
  1. 减少内存占用:通过共享相同的数据来减少内存占用,提高系统的性能。
  2. 提高效率:享元模式使得系统可以高效地管理大量对象,减少了对象的创建和销毁开销。
  3. 灵活性:享元模式区分内部状态和外部状态,使得对象可以灵活地适应不同的环境。
享元模式的缺点:
  1. 复杂性增加:享元模式引入了更多的类和接口,使得系统复杂度增加。
  2. 依赖外部状态:享元模式需要客户端代码传递外部状态,增加了客户端代码的复杂性。
  3. 调试难度增加:由于享元对象的共享特性,调试时可能会比较困难。

享元模式在实际应用中的例子包括Java中的字符串常量池、数据库连接池、线程池等,这些场景中享元模式通过共享对象来提高性能和减少资源消耗。

池化方案

数据库连接池化方案是一种在应用程序和数据库之间管理数据库连接的技术,它通过预先创建一定数量的数据库连接,并在应用程序运行过程中复用这些连接,从而提高系统性能和资源利用效率。

工作原理
  1. 连接池的建立:在系统初始化时,根据配置创建一定数量的数据库连接,并存储在连接池中。
  2. 连接池中连接的使用管理:当应用程序需要访问数据库时,它从连接池中获取一个可用连接,使用完毕后再将连接返回池中,而不是关闭连接。
  3. 连接池的关闭:应用程序关闭时,释放所有连接并清理连接池。
优势
  1. 减少网络开销:通过复用连接减少了建立和关闭连接的频率。
  2. 提升系统性能:减少了连接创建和销毁的开销,提高了数据库访问效率。
  3. 避免TIME_WAIT状态:通过连接复用避免了频繁的连接关闭导致的TIME_WAIT状态问题。
实现方式
  1. 标准接口:使用DataSource接口,它是Java中管理数据库连接的标准方式。
  2. 获取和归还连接:通过getConnection()方法获取连接,使用完毕后通过Connection.close()方法归还连接。
常用连接池技术
  1. HikariCP:以其高性能、低延迟和易用性而闻名,是许多应用的首选。
  2. Druid:由阿里巴巴开发的数据库连接池,提供了丰富的监控和统计功能。
  3. C3P0DBCPTomcat Jdbc PoolBoneCP:这些都是常用的主流开源数据库连接池。
配置与调优

配置连接池时,需要根据应用的实际情况和数据库的性能来设置参数,如最大连接数、最小空闲连接数、连接超时时间等。同时,启用连接校验功能以确保连接的稳定性和可靠性

业务背景

在第5章节,我们实现了解析 XML 中的 数据源的配置信息,为了方便理解,直接使用了Druid实现数据源的创建,完成数据库的操作。Mybatis 中是有自己的数据源实现的,包括无池化的 UnpooledDataSource 实现方式和有池化的 PooledDataSource 实现方式。

本章节我们就来实现一下池化数据源的处理,实现过程涉及一些数据源的配置属性, 包括:最大活跃连接数、空闲连接数、检测时长等。通过本章节的学习,我们也会了解这些属性在连接池中所起到的作用。

目标

基于当前框架实现数据源连接的池化处理,熟悉配置数据源时涉及的关键属性的作用。

设计

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

image-20241217142610333

  • 通过提供统一的连接池中心,存放数据源连接,并根据配置,按照请求获取连接的操作,创建连接池的数据源连接数量。这里就包括了最大空闲连接和最大活跃连接,都随着创建过程被控制。
  • 此外由于控制了连接池中连接的数量,所以当外部从连接池获取连接时,如果连接已满则会进行循环等待。

实现

工程代码

image-20241217142826734

类图

image-20241217142854017
  • 在 Mybatis 数据源的实现中,包括无池化的 UnpooledDataSource 实现类和有池化的 PooledDataSource 实现类, PooledDataSource 对UnpooledDataSource 进行扩展处理。把创建出来的连接保存到内存中,记录为空闲连接和活跃连接,在不同的阶段进行使用。
  • PooledConnection 是对连接的代理操作,通过invoke方法的反射调用,对关闭的连接进行回收处理,并使用 notifyAll 通知正在等待连接的用户进行抢连接。
  • 另外是对 DataSourceFactory 数据源工厂接口的实现,由无池化工厂实现后,有池化工厂继承的方式进行处理,这里没有太多的复杂操作,池化的处理主要集中在 PooledDataSource 类中进行处理。

实现步骤

1.无池化数据源

对于数据库连接池的实现,不一定非得提供池化技术,对于某些场景可以只使用无池化的连接池。那么在实现的过程中,可以把无池化的实现和池化实现拆分解耦,在需要的时候只需要配置对应的数据源即可。

1-1 定义无池化的数据源UnpooledDataSource
  • 无池化的数据源连接实现比较简单,核心在于 initializerDriver 初始化驱动中使用了 Class.forName 和 newInstance 的方式创建了数据源连接操作。
  • 在创建完成连接以后,把连接存放到驱动注册器中,方便后续使用中可以直接获取连接,避免重复创建所带来的资源消耗。
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;

    // 数据库链接地址
    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);
        }
    }

    /**
     * 驱动代理类.
     *
     * @return
     * @throws SQLException
     */
    private static class DriverProxy implements Driver {

        private Driver driver;

        DriverProxy(Driver driver) {
            this.driver = driver;
        }

        @Override
        public Connection connect(String url, Properties info) throws SQLException {
            return this.driver.connect(url, info);
        }

        @Override
        public boolean acceptsURL(String url) throws SQLException {
            return this.driver.acceptsURL(url);
        }

        @Override
        public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
            return this.driver.getPropertyInfo(url, info);
        }

        @Override
        public int getMajorVersion() {
            return this.driver.getMajorVersion();
        }

        @Override
        public int getMinorVersion() {
            return this.driver.getMinorVersion();
        }

        @Override
        public boolean jdbcCompliant() {
            return this.driver.jdbcCompliant();
        }

        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return this.driver.getParentLogger();
        }
    }


    /**
     * 初始化驱动.
     *
     * @return
     * @throws SQLException
     */
    private synchronized void initializerDriver() throws SQLException {
        // 判断是否已经注册过驱动
        if (!registeredDrivers.containsKey(driver)) {
            try {
                Class<?> driverType = Class.forName(driver, true, driverClassLoader);
                Driver driverInstance = (Driver) driverType.getDeclaredConstructor().newInstance();
                DriverManager.registerDriver(new DriverProxy(driverInstance));
                registeredDrivers.put(driver, driverInstance);

            } catch (Exception e) {
                throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
            }
        }
    }


    private Connection doGetConnection(String username, String password) throws SQLException {
        Properties props = new Properties();
        if (driverProperties != null) {
            props.putAll(driverProperties);
        }
        if (username != null) {
            props.setProperty("user", username);
        }
        if (password != null) {
            props.setProperty("password", password);
        }
        return doGetConnection(props);


    }

    private Connection doGetConnection(Properties props) throws SQLException {
        // 初始化驱动
        initializerDriver();
        // 获取链接
        Connection connection = DriverManager.getConnection(url, props);
        if (autoCommit != null && autoCommit != connection.getAutoCommit()) {
            connection.setAutoCommit(autoCommit);
        }
        if (defaultTransactionIsolationLevel != null) {
            connection.setTransactionIsolation(defaultTransactionIsolationLevel);
        }
        //打印日志
        System.out.println("获取链接信息:" + connection.toString());
        return connection;
    }


    @Override
    public Connection getConnection() throws SQLException {
        return doGetConnection(username, password);
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return doGetConnection(username, password);
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        throw new SQLException(getClass().getName() + " is not a wrapper.");
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return DriverManager.getLogWriter();
    }

    @Override
    public void setLogWriter(PrintWriter logWriter) throws SQLException {
        DriverManager.setLogWriter(logWriter);
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        DriverManager.setLoginTimeout(seconds);
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return DriverManager.getLoginTimeout();
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
    }


    /**
     * get/set属性.
     */
   ......

}

1-2 定义无池化的数据源工厂UnpooledDataSourceFactory
  • 简单包装 getDataSource 获取数据源处理,把必要的参数进行传递过去。在 Mybatis 源码中这部分则是进行了大量的反射字段处理的方式进行存放和获取的。
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;
    }
}

1-3 在configuration无参构造方法里,增加注册无池化数据源工厂类
public class Configuration {

    /**
     * 映射注册机.
     */
    protected MapperRegistry mapperRegistry = new MapperRegistry();

    /**
     * 映射的语句,存在Map.
     */
    protected final Map<String, MappedStatement> mappedStatementMap = new HashMap<>();

    /**
     * 类型别名注册机.
     */
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

    /**
     * 环境.
     */
    protected Environment environment;


    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("DRUID", DruidDataSourceFactory.class);
        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    }

    public TypeAliasRegistry getTypeAliasRegistry() {
        return typeAliasRegistry;
    }

    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }

    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

    public boolean hasMapper(Class<?> type) {
        return mapperRegistry.hasMapper(type);
    }

    public void addMappedStatement(MappedStatement ms) {
        mappedStatementMap.put(ms.getId(), ms);
    }

    public MappedStatement getMappedStatement(String id) {
        return mappedStatementMap.get(id);
    }

    public Environment getEnvironment() {
        return environment;
    }

    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}

2. 池化数据源

有池化的数据源链接,核心在于对无池化链接的包装,同时提供了相应的池化技术实现,包括:pushConnectionpopConnectionforceCloseAllpingConnection 的操作处理。

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

2-1 池化连接代理PooledConnection
  • 对连接进行池化处理,所以当连接调用一些 CLOSE 方法的时候,也需要把连接从池中关闭和恢复可用,允许其他用户获取到连接。那么这里就需要对连接类进行代理包装,处理 CLOSE 方法。
  • 通过 PooledConnection 实现 InvocationHandler#invoke 方法,包装代理链接,这样就可以对具体的调用方法进行控制了。
  • 在 invoke 方法中处理对 CLOSE 方法控制以外,排除 toString 等Object 的方法后,则是其他真正需要被 DB 链接处理的方法了。
  • 那么这里有一个对于 CLOSE 方法的数据源回收操作 dataSource.pushConnection(this); 有一个具体的实现方法,在池化实现类 PooledDataSource 中进行处理。
public class PooledConnection implements InvocationHandler {


    private static final String CLOSE = "close";

    //代理的接口
    private static final Class<?>[] IFACES = new Class<?>[]{Connection.class};

    private int hashCode = 0;

    // 池化数据源.
    private PooledDataSource dataSource;

    // 真实的链接.
    private Connection realConnection;

    // 代理的链接.
    private Connection proxyConnection;

    // 检出时间戳.
    private long checkoutTimestamp;

    // 创建时间戳.
    private long createdTimestamp;

    // 最近使用时间戳.
    private long lastUsedTimestamp;

    // 链接类型编码.
    private int connectionTypeCode;

    // 是否有效.
    private boolean valid;

    public PooledConnection(Connection connection, PooledDataSource dataSource) {
        this.hashCode = connection.hashCode();
        this.realConnection = connection;
        this.dataSource = dataSource;
        this.createdTimestamp = System.currentTimeMillis();
        this.lastUsedTimestamp = System.currentTimeMillis();
        this.valid = true;
        this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
    }

    @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);
        }
    }

    private void checkConnection() throws SQLException {
        if (!valid) {
            throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
        }
    }


    // 将链接失效.
    public void invalidate() {
        valid = false;
    }

    // 链接是否有效.
    public boolean isValid() {
        return valid && realConnection != null && dataSource.pingConnection(this);
    }

    // 获取真实的链接.
    public Connection getRealConnection() {
        return realConnection;
    }

    // 获取代理的链接.

    public Connection getProxyConnection() {
        return proxyConnection;
    }

   	// 其他属性的 get/set 方法.
    ......

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof PooledConnection) {
            return realConnection.hashCode() == (((PooledConnection) obj).realConnection.hashCode());
        } else if (obj instanceof Connection) {
            return hashCode == obj.hashCode();
        } else {
            return false;
        }
    }

}

2-2 池化数据源 PooledDataSource #pushConnection 放入连接
  • 在 PooledDataSource#pushConnection 数据源回收的处理中,核心在于判断链接是否有效,以及进行相关的空闲链接校验,判断是否把连接回收到 idle 空闲链接列表中,并通知其他线程来抢占。
  • 如果现在空闲链接充足,那么这个回收的链接则会进行回滚和关闭的处理中。connection.getRealConnection().close();
  /**
     * 放入链接资源.
     *
     * @param connection
     * @throws SQLException
     */
    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.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++;
            }

        }
    }
2-3 池化数据源 PooledDataSource #popConnection 取出连接
  • popConnection 获取链接是一个 while 死循环操作,只有获取到链接抛异常才会退出循环。
  • 获取链接的过程会使用 synchronized 进行加锁,因为所有线程在资源竞争的情况下,都需要进行加锁处理。在加锁的代码块中通过判断是否还有空闲链接进行返回,如果没有则会判断活跃连接数是否充足,不充足则进行创建后返回。在这里也会遇到活跃链接已经进行循环等待的过程,最后再不能获取则抛出异常。
    // 取出链接资源.
    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.");
                        }
                    }
                }
            }
        }

        if (conn == null) {
            logger.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
            throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
        }

        return conn;
    }
2-4 池化数据源工厂
  • 有池化的数据源工厂实现的也比较简单,只是继承 UnpooledDataSourceFactory 共用获取属性的能力,以及实例化出池化数据源即可。
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;
    }
}

2-5 新增类型别名注册器

  • 新增两个数据源和对应的工厂实现类以后,则需要把它们配置到 Configuration 中,这样才能在解析 XML 时候根据不同的数据源类型获取和实例化对应的实现类。
public class Configuration {

    /**
     * 映射注册机.
     */
    protected MapperRegistry mapperRegistry = new MapperRegistry();

    /**
     * 映射的语句,存在Map.
     */
    protected final Map<String, MappedStatement> mappedStatementMap = new HashMap<>();

    /**
     * 类型别名注册机.
     */
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

    /**
     * 环境.
     */
    protected Environment environment;


    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("DRUID", DruidDataSourceFactory.class);
        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    }

    public TypeAliasRegistry getTypeAliasRegistry() {
        return typeAliasRegistry;
    }

    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }

    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }

    public boolean hasMapper(Class<?> type) {
        return mapperRegistry.hasMapper(type);
    }

    public void addMappedStatement(MappedStatement ms) {
        mappedStatementMap.put(ms.getId(), ms);
    }

    public MappedStatement getMappedStatement(String id) {
        return mappedStatementMap.get(id);
    }

    public Environment getEnvironment() {
        return environment;
    }

    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}

测试

事先准备

创建库表

-- 建表
CREATE TABLE `my_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` varchar(9) DEFAULT NULL COMMENT '用户ID',
  `user_head` varchar(16) DEFAULT NULL COMMENT '用户头像',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
  `user_name` varchar(64) DEFAULT NULL COMMENT '用户名',
  `user_password` varchar(64) DEFAULT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;


-- 插入数据
INSERT INTO my_user (user_id, user_head, create_time, update_time, user_name, user_password) VALUES('1', '头像', '2024-12-13 18:00:12', '2024-12-13 18:00:12', '小苏', 's123asd');

定义一个数据库接口 IUserDao

IUserDao

public interface IUserDao {

    String queryUserInfoById(String uid);

}

配置数据源

  • 通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
  • 这里DataSource 配置的是 DRUID,因为我们实现的是这个数据源的处理方式。
<?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>


    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="UNPOOLED">  #无池化时配置成这个类型值
            <dataSource type="POOLED">  #池化时配置城这个类型值
                <property name="driver" value="com.mysql.cj.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>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
    </mappers>
</configuration>

定义对应的mapper xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.suwg.mybatis.test.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.suwg.mybatis.test.po.User">
        SELECT id, user_id, user_head, create_time
        FROM user
        where id = #{id}
    </select>

</mapper>

测试用例

  • 在无池化和有池化的测试中,基础的单元测试类不需要改变,仍是通过 SqlSessionFactory 中获取 SqlSession 并获得映射对象和执行方法调用。另外这里是添加了50次的查询调用,便于验证连接池的创建和获取以及等待。
  • 变化的在于 mybatis-config-datasource.xml 中 dataSource 数据源类型的调整 dataSource type="POOLED/UNPOOLED"
单元测试(无池化数据源)
    <dataSource type="UNPOOLED">  #无池化时配置成这个类型值
public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    // 测试无池化的数据源.
    // 每次都会创建
    @Test
    public void testUnpooledDataSource() 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));
        }
    }


}

测试结果(无池化数据源)

image-20241217152230848

  • 从输出的结果来看,无池化的连接池操作,会不断的与数据库建立新的连接并执行 SQL 操作,这个过程中只要数据库还有连接可以用,就会不断创建。
单元测试(池化数据源)
    <dataSource type="POOLED">  #池化时配置成这个类型值
public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void testPooledDataSource() 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 < 100; i++) {
            User user = userDao.queryUserInfoById(1L);
            logger.info("测试结果:{}", JSON.toJSONString(user));
        }

    }


}

测试结果(池化数据源)

image-20241217152428628

  • 从输出的结果来看,通过使用连接池的配置可以看到,在调用和获取连接的过程中,当调用次数打到5次以后,连接池中就有了5个活跃的链接,再调用的时候则需要等待连接释放后才能使用并执行 SQL 操作。
  • 测试的过程中还包括了连接的空闲数量、活跃数量、关闭、异常等,大家可以在学习的过程中进行验证处理。

总结

  • 本章节我们完成了 Mybatis 数据源池化的设计和实现,通过这样的分析、实现、验证的过程,让大家更加理解我们平常使用的连接池所遇到的一些真实问题都是怎么发生的。
  • 另外关于连接池的实现重点可以随着调试验证的过程中进行学习,包括:synchronized 加锁、创建连接、活跃数量控制、休眠等待时长,抛异常逻辑等,这些都与我们日常使用连接池时的配置息息相关。
  • Mybatis 的数据源虽然可以直接使用 Druid 处理,但只有动手自己实现一遍数据源连接池才能更好的理解池化技术的落地方案,也能为以后做此类功能时,有一个可落地的具体方案。
  • 我们日常使用DB连接池时,如果一个SQL操作引起了慢查询,则会导致整个服务进入瘫痪的阶段,各个和数据库相关的接口调用,都不能获得到连接,会导致接口查询TP99陡然增高,系统开始大量报警。
  • 连接池的配置要和数据库所分配的连接池对应上,避免应用配置连接池超过数据库所提供的连接池数量,出现夯住不能分配连接的问题,导致数据库拖垮从而引起连锁反应。

参考书籍:《手写Mybatis渐进式源码实践》

书籍源代码:https://github.com/fuzhengwei/book-small-mybatis

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值