相关阅读:
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. 理清资源池概念
属性 | 描述 |
---|---|
1-已使用 | 连接池中已经被使用的资源 |
2-已有资源 | 连接池中已有的资源,包括在用的和闲置的 |
3-最大资源 | 连接池中允许存在的资源上线,包括在用的和闲置,以及待扩展的 |
4-闲置资源 | 连接池中未使用的资源,当池中的资源都没有被使用时,闲置资源=已有资源,闲置资源的作用是缓存资源,以在需要使用资源时能快速获取 |
2.2. 资源状态
如何获取到可用资源有两种方式:
1.--显示区分资源池,闲置资源放入闲置资源池,获取时从闲置资源池中取出,使用完之后再放入闲置资源池中,此方式需要对闲置资源池加锁。
2.--不区分资源池,通过资源状态来标记资源是否可用,获取时通过CAS操作设置资源状态,如果设置成功则获取到资源,类似如下的方式获取资源:
resource.compareAndSetState(NOT_IN_USE, IN_USE);
这里采用方式2,因此设计资源状态如下:
属性 | 描述 |
---|---|
NOT_IN_USE | 资源创建后的初始状态 |
IN_USE | 资源被占用后的状态 |
REMOVED | 资源被关闭后的状态,例如资源池收缩时关闭多余的闲置资源,之所以有这个状态是因为没有使用同步原语锁定资源池,需要先修改资源状态,使得资源不可用,然后再进行后续处理 |
2.3. 资源封装
2.3.1. 类设计
由于不可能修改原始资源对象的代码来记录资源状态,因此需要一个资源封装类来封装原始资源对象,以数据库的Connection为例,类关系如下:
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中注入这个实现类,通过委托的方式去调用,类结构如下:
抽象类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()方法将,连接释放到资源池中,不能通过别的方式来释放资源,但该类的实现类是无法修改的,因此需要通过一个代理类来完成资源释放到池中的操作,类结构如下:
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. 定制类视图
在通用框架之外,定制类视图如下:
类 | 描述 |
---|---|
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比较重,类关系复杂,无关代码较多,很多场景也用不上。