【铿然架构实战】通用资源池管理框架

597 阅读10分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 85 篇原创文章

相关阅读:

JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
Java编程思想(五)事件通知模式解耦过程
Java编程思想(六)事件通知模式解耦过程
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃
HikariPool源码(二)设计思想借鉴
【极客源码】JetCache源码(一)开篇
【极客源码】JetCache源码(二)顶层视图
人在职场(一)IT大厂生存法则


1. 资源池概述

1.1. 什么资源需要通过资源池管理

一般来说,需要通过资源池管理的资源有以下特征:

1.--资源使用频繁,创建资源比较耗时,不能每次使用时才创建,用完即关闭,下次使用时再重新创建,需要预先创建并缓存在池中。

2.--资源使用短暂,用完即释放,不会长期占用。

1.2. 资源池管理需具备的能力

资源池管理应具备如下能力:

1.--动态伸缩,当需要资源时可以动态增加资源,不需要时可以释放资源,并保持一定数量的闲置资源,以在使用时快速获取。

2.--资源泄露监控,如果资源被占用后,长时间未释放,那么可能存在资源泄露,最终会导致无资源可用,因此要能监控到资源泄露。

3.-- 资源池的健康状况监控

1.3. 通用资源池框架设计考虑

1.--适用所有频繁、短暂使用,创建耗时的资源管理

2.--对于同类资源,当存在部分场景需要长久占用时,可以增加获取资源参数,设置允许使用最大时长,避免资源泄露告警,增加灵活性

3.--当引入新的资源类型时,不需要修改任何核心代码即能适配

4.--资源管理的并发机制是使用同步原语还是使用CAS来处理。(注:CAS的性能可能高于同步原语)

5.--同步还是异步,一些操作比较耗时,可以使用异步任务来完成,以提升调用者的性能。

2. 资源池管理框架设计和核心代码

2.1. 理清资源池概念

01资源池.jpg

属性描述
1-已使用连接池中已经被使用的资源
2-已有资源连接池中已有的资源,包括在用的和闲置的
3-最大资源连接池中允许存在的资源上线,包括在用的和闲置,以及待扩展的
4-闲置资源连接池中未使用的资源,当池中的资源都没有被使用时,闲置资源=已有资源,闲置资源的作用是缓存资源,以在需要使用资源时能快速获取

2.2. 资源状态

如何获取到可用资源有两种方式:

1.--显示区分资源池,闲置资源放入闲置资源池,获取时从闲置资源池中取出,使用完之后再放入闲置资源池中,此方式需要对闲置资源池加锁。

2.--不区分资源池,通过资源状态来标记资源是否可用,获取时通过CAS操作设置资源状态,如果设置成功则获取到资源,类似如下的方式获取资源:

resource.compareAndSetState(NOT_IN_USE, IN_USE);

这里采用方式2,因此设计资源状态如下:

02资源状态.jpg

属性描述
NOT_IN_USE资源创建后的初始状态
IN_USE资源被占用后的状态
REMOVED资源被关闭后的状态,例如资源池收缩时关闭多余的闲置资源,之所以有这个状态是因为没有使用同步原语锁定资源池,需要先修改资源状态,使得资源不可用,然后再进行后续处理

2.3. 资源封装

2.3.1. 类设计

由于不可能修改原始资源对象的代码来记录资源状态,因此需要一个资源封装类来封装原始资源对象,以数据库的Connection为例,类关系如下:

03资源封装.jpg

1.--ResourceEntry使用泛型,这样对于其他类型的资源,如HTTP连接,RPC连接也可以直接使用。

2.--ResourceEntry上定义了状态属性,通过状态变化标识资源是否可用。

2.3.2. 主要代码ResourceEntry

public class ResourceEntry<T> {
    private static final Logger logger = LoggerFactory.getLogger(ResourceEntry.class);

    private long lastIdleStart;  // 上次闲置开始时间,用于在闲置超时后判断,并回收资源
    private T t;                 //实际的资源,例如数据库连接 Connection
    private volatile int state = ResourceEntryState.NOT_IN_USE.value();
    private static final AtomicIntegerFieldUpdater<ResourceEntry> stateUpdater;
    private ProxyLeakTask proxyLeakTask;

    static
    {
        stateUpdater = AtomicIntegerFieldUpdater.newUpdater(ResourceEntry.class, "state");
    }

    ResourceEntry(T t) {
        this.t = t;
        lastIdleStart = ClockUtil.now();
        logger.debug("resource be created. [{}]", t);
    }

    T get() {
        lastIdleStart = Long.MAX_VALUE;
        return t;
    }

    T close() {
        cancleLeakTask();
        return t;
    }

    void cancleLeakTask() {
        if (proxyLeakTask != null) {
            proxyLeakTask.cancel();
        }
    }

    boolean compareAndSetState(ResourceEntryState expect, ResourceEntryState update) {
        return stateUpdater.compareAndSet(this, expect.value(), update.value());
    }

    int getState() {
        return stateUpdater.get(this);
    }

    public long getLastIdleStart() {
        return lastIdleStart;
    }

    public void setLastIdleStart(long lastIdleStart) {
        this.lastIdleStart = lastIdleStart;
    }

    void setProxyLeakTask(ProxyLeakTask proxyLeakTask) {
        this.proxyLeakTask = proxyLeakTask;
    }
}

2.4. 资源的创建和关闭

2.4.1. 类设计

对于资源池管理通用框架,定制者会创建什么类型的资源事先并不知道,因此需要定义资源管理接口,由定制者自行去实现资源的相关操作,并在Pool中注入这个实现类,通过委托的方式去调用,类结构如下:

04资源管理者.jpg

抽象类ResourceManager的方法描述如下:

方法描述
create创建资源
quietlyClose关闭资源,物理关闭,不是释放到池中

2.4.2. ResourceManager

public abstract class ResourceManager<T> {
    protected Pool pool;

    /**
     * 创建物理资源
     *
     * @return 物理资源
     */
    public abstract T create();

    /**
     * 安静关闭物理资源,不抛出异常,在资源池动态收缩的时候调用
     *
     * @param t 物理资源
     * @param closureReason 关闭原因
     */
    public abstract void quietlyClose(T t, String closureReason);

    void setPool(Pool pool) {
        this.pool = pool;
    }
}

2.5. 资源释放

2.5.1. 类设计

在实际使用数据库连接时,是通过java.sql.Connection.close()方法将,连接释放到资源池中,不能通过别的方式来释放资源,但该类的实现类是无法修改的,因此需要通过一个代理类来完成资源释放到池中的操作,类结构如下:

05代理.jpg

1.--绿色部分是定制相关的类,对于不同的资源类型处理方式类似。

2.--资源的实际创建和关闭通过DBConnectionResourceManager来完成,在创建Connection后,可以创建ProxyConnection对象,修改其close方法达到释放资源到池中的目的,并包装Connection后返回给调用者,这样外部调用的是ProxyConnection,ProxyConnection再委托给Connection。

2.5.2. ProxyConnection

public class ProxyConnection implements Connection {
    private Connection connection;
    private Pool pool;
    
    //构造器传入connection
    public ProxyConnection(Pool pool, Connection connection) {
        this.pool = pool;
        this.connection = connection;
    }
    
    @Override
    public void close() throws SQLException {
        //释放到资源池中,并没有真正关闭连接,注意这里释放的是this,不是Connection.
        pool.releaseResource(this);
    }
    
    @Override
    public void abort(Executor executor) throws SQLException {
        pool.abortResource(this); //需要释放对应的ResourceEntry
        connection.abort(executor);
    }
   
    //如下所示,其他的方法都是委托给connection调用。
    @Override
    public boolean isClosed() throws SQLException {
        return connection.isClosed();
    }

    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        return connection.getMetaData();
    }

2.6. 资源泄露监控

资源监控的原理是在获取资源时启动一个定时任务,执行的时间为允许源占用的时间间隔之后,如果任务执行了则说明发生了资源泄露。

在资源释放时需要取消定制任务的执行,否则会误判。

2.6.1. Pool

    public T getResource(final long hardTimeoutMillis, long leakDetectionThresholdMillis) throws Exception {
        logger.debug("getResource - hardTimeoutMillis:[{}].", hardTimeoutMillis);
        try {
            ResourceEntry resourceEntry = borrow(hardTimeoutMillis);
            if (resourceEntry == null) {
                throw new Exception(poolName + " - Resource is not available.");
            }

            //调度资源泄露检查任务,检查资源是否有泄露
            if (leakDetectionThresholdMillis > Constants.MIN_LEAK_DETECTION_THRESHOLD_MILLIS) {
                taskScheduler.scheduleLeakTask(resourceEntry, leakDetectionThresholdMillis);
            }
            return (T) resourceEntry.get();

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new Exception(poolName + " - Interrupted during resource acquisition", e);
        }
    }

2.6.2. ProxyLeakTask

public class ProxyLeakTask implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(ProxyLeakTask.class);

    private ScheduledFuture<?> scheduledFuture;
    private String resourceName;
    private Exception exception;
    private String threadName;
    private boolean isLeaked;

    ProxyLeakTask(final ResourceEntry resourceEntry) {
        this.exception = new Exception("Apparent resource leak detected");
        this.threadName = Thread.currentThread().getName();
        this.resourceName = resourceEntry.get().toString();
        resourceEntry.setProxyLeakTask(this);
    }

    @Override
    // 一旦被执行,说明获取连接到关闭超过了leakDetectionThreshold时间
    public void run() {
        isLeaked = true;

        final StackTraceElement[] stackTrace = exception.getStackTrace();
        final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5];
        System.arraycopy(stackTrace, 5, trace, 0, trace.length);

        exception.setStackTrace(trace);
        // 下面是监控到连接泄漏的处理,这里只是记录到日志中,如果通过一个接口处理,并可以让使用者动态实现会更灵活
        logger.warn("Resource leak detection triggered for {} on thread {}, stack trace follows", resourceName, threadName, exception);
    }

    public void cancel() {
        scheduledFuture.cancel(false);
        if (isLeaked) {  // 检查到泄漏后连接被关闭,则给一个提示信息
            logger.info("Previously reported leaked resource {} on thread {} was returned to the pool (unleaked)", resourceName, threadName);
        }
    }

    void set(ScheduledFuture scheduledFuture) {
        this.scheduledFuture = scheduledFuture;
    }
}

2.7. 资源池容量动态伸缩

可以通过一个固定周期执行的定时任务来负责资源池的伸缩,其主要职责是维持闲置资源数量,当不够的时候增加,多的时候释放。

2.7.1. HouseKeeper.java

class HouseKeeper implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(HouseKeeper.class);

    private Pool pool;

    public HouseKeeper(Pool pool) {
        this.pool = pool;
    }
    @Override
    public void run() {
        try {
            pool.shrinkPool();
            pool.fillPool();
        }
        catch (Exception e) {
            logger.error("Unexpected exception in housekeeping task", e);
        }
    }
}

2.7.2. Pool.java

    void shrinkPool() {
        final long idleTimeout = config.getIdleTimeoutMillis();
        final long elapsedMillis = ClockUtil.elapsedMillis();

        if (idleTimeout > 0L && config.getMinimumIdleSize() < config.getMaximumPoolSize()) {
            logPoolState("Before cleanup");

            final List<ResourceEntry> notInUse = values(ResourceEntryState.NOT_IN_USE);
            int toRemove = notInUse.size() - config.getMinimumIdleSize();
            for (ResourceEntry entry : notInUse) {
                if (toRemove > 0 && ClockUtil.elapsedMillis(entry.lastIdleStart, elapsedMillis) > idleTimeout) {
                    closePoolEntry(entry, "(connection has passed idleTimeout)");
                    toRemove--;
                }
            }
            logPoolState("After cleanup");
        }
    }
 

    synchronized void fillPool() {
        final int resourcesToAdd = Math.min(config.getMaximumPoolSize() - getTotalResources(),
                config.getMinimumIdleSize() - getIdleResources()) - addEntryQueue.size();
        for (int i = 0; i < resourcesToAdd; i++) {
            taskScheduler.submitCreateTask(poolEntryCreator);
        }
    }

2.8. 定制类视图

在通用框架之外,定制类视图如下:

定制视图.jpg

描述
DBConnectionPoolStartup连接池创建启动类,所有的连接池都在这里创建,在进程启动时调用
DBConnectionPoolRegistry连接池注册类,注册数据源名称和池的对应关系,用于通过data source获取连接池
DBConnectionFactory连接工厂,客户端只和这个类打交道,获取连接
DBConnectionResourceManager负责资源的创建和关闭
ProxyConnection连接代理类,用于调用close方法时,释放连接到池中

以上类除了DBConnectionResourceManager和ProxyConnection是强依赖,必须实现之外,其他类都可根据需要自行实现。

2.8.1. DBConnectionPoolStartup

public class DBConnectionPoolStartup {
    private static AtomicBoolean start = new AtomicBoolean(false);
    /**
     * 创建资源池,所有资源池统一在这里创建,方法内部可以考虑异步线程调用,不阻塞主线程
     */
    public static void start() {
        if (start.compareAndSet(false, true)) {
            //实际的资源创建类
            DBConnectionResourceManager resourceAdaptor = new DBConnectionResourceManager();
            Config config = new Config();  //使用配置,也可以通过其他方式读取配置然后重置。
            String poolName = "OrderDB";
            Pool<Connection> pool = new Pool(poolName, resourceAdaptor, config);
            DBConnectionPoolRegistry.put(poolName, pool);
        }
    }
}

2.8.2. DBConnectionPoolRegistry

public class DBConnectionPoolRegistry {
    private static Map<String, Pool<Connection>> poolMap = new ConcurrentHashMap<>();

    public static Pool<Connection> getPool(String poolName) {
        if (poolMap.containsKey(poolName)) {
            return poolMap.get(poolName);
        }
        return null;
    }

    public static void put(String poolName, Pool<Connection> pool) {
        poolMap.put(poolName, pool);
    }
}

2.8.3. DBConnectionFactory

public class DBConnectionFactory {
    public static Connection getConnection(String dataSource) throws Exception {
        Pool<Connection> pool = DBConnectionPoolRegistry.getPool(dataSource);
        if (pool != null) {
            return pool.getResource(2, 3);
        } else {
            throw new Exception("unknown data source.");
        }
    }
}

2.8.4. DBConnectionResourceManager

public class DBConnectionResourceManager extends ResourceManager<Connection> {
    private static final Logger logger = LoggerFactory.getLogger(DBConnectionResourceManager.class);

    @Override
    public void quietlyClose(Connection connection, String closureReason) {
        logger.debug("quietlyClose - connection:[{}].", connection);
        try {
            connection.close();
        } catch (Exception e) {
            logger.error("close connection error.", e);
        }
    }


    @Override
    public Connection create() {
        logger.debug("create connection.");
        //创建连接,这里模拟创建一个
        Connection connection = new MockDBConnection();
        //创建一个代理类,通过代理类的close方法调用pool.releaseResource(connection)方法来释放资源
        ProxyConnection proxyConnection = new ProxyConnection(pool, proxyLeakTask, connection);
        return proxyConnection;
    }
}

3. 其他

以上描述了通用资源池管理框架主要思路,按照这些已经基本可以运行起来,且可应用于除了数据库连接之外的其他资源池。

设计和编码参考了开源项目HikariCP,HikariCP比较重,类关系复杂,无关代码较多,很多场景也用不上。


<--阅过留痕,左边点赞!