zoukankan      html  css  js  c++  java
  • 如何快速实现一个连接池?

    文章首发于公众号「陈树义」及个人博客 shuyi.tech,欢迎关注访问。

    在实际工作中,我们经常会用到各种连接池,例如:连接 FTP 服务器的连接数有限,需要建立一个连接池;连接数据库的连接数有限,需要建立一个连接池。那我们如何去快速实现一个连接池呢?

    无论是 FTP 连接池,还是数据库连接池,我们会发现它们都有相同的地方,它们都需要:生命周期管理、连接创建管理等等。如果我们从零开始去实现这些功能,那我们要耗费的时间就很长了!那有没有一个通用的库可以快速实现一个线程池呢?

    得益于 Java 完善的生态,前人们针对这种需要开发了一个通用库:Apache Commons Pool(下文简称 ACP)。本质上来说,ACP 库提供的是管理对象池的通用能力,当然也可以用来管理连接池了!

    什么是 ACP?

    ACP 库提供了一整套用于实现对象池化的 API,以及若干种各具特色的对象池实现。目前最常用的版本是 2.0 版本,相对于 1.x 版本而言,并不是简单升级。2.0 版本是对象池实现的完全重写,显著的提升了性能和可伸缩性,并且包含可靠的实例跟踪和池监控。

    Apache Commons Pool 的官网地址为:Pool – Overview,想翻找相关文档资料,到这里去是最权威、最全面的。

    如何使用 ACP?

    要使用 ACP 实现一个线程池,首先需要先引入 ACP 的依赖包,这里以 Maven 为例。

    <dependency>
    	<groupId>org.apache.commons</groupId>
    	<artifactId>commons-pool2</artifactId>
    	<version>2.0</version>
    </dependency>
    

    要使用 ACP 实现一个对象池,大致可以分为三个步骤:

    • 创建对象工厂:告诉 ACP 如何创建你要的对象。
    • 创建对象池:告诉 ACP 你想创建一个怎样的对象池。
    • 使用对象池:ACP 告诉你如何使用你的对象。

    创建对象工厂

    文章首发于公众号「陈树义」及个人博客 shuyi.tech,欢迎关注访问。

    对象工厂告诉 ACP,它应该如何去创建、激活、钝化、销毁你的对象。创建对象工厂非常简单,只需要实现 ACP 的 PooledObjectFactory 接口即可。PooledObjectFactory 接口的定义如下:

    public interface PooledObjectFactory<T> {
      PooledObject<T> makeObject() throws Exception;
      void destroyObject(PooledObject<T> p) throws Exception;
      boolean validateObject(PooledObject<T> p);
      void activateObject(PooledObject<T> p) throws Exception;
      void passivateObject(PooledObject<T> p) throws Exception;
    }
    

    但更多情况下,我们会继承 BasePooledObjectFactory 类来实现对象工厂。因为 BasePooledObjectFactory 类是 PooledObjectFactory 的基础实现类,使用它可以帮我们省了很多麻烦。通过继承这个抽象类,我们只需要实现两个方法:create() 和 wrap() 方法。

    // 告诉 ACP 如何创建对象
    public abstract T create() throws Exception;
    // 定义你要返回的对象
    public abstract PooledObject<T> wrap(T obj);
    

    create() 方法定义你的对象初始化过程,最后将初始化完成的对象返回。例如你想定义一个 SFTP 的连接,那么你首先需要定义一个 JSch 对象,之后设置账号密码,之后连接服务器,最后返回一个 ChannelSftp 对象。

    public ChannelSftp create() {
        // SFTP 连接的创建过程
    }
    

    wrap() 方法定义你要返回的对象,对于一个 SFTP 的连接池来说,其实就是一个 ChannelSftp 对象。一般情况下可以使用类 DefaultPooledObject 替代,参考实现如下:

    @Override
    public PooledObject<Foo> wrap(Foo foo) {
        return new DefaultPooledObject<Foo>(foo);
    }
    

    创建对象池

    创建好对象工厂之后,ACP 已经知道你需要的对象如何创建了。那么接下来,你需要根据你的实际需要,去创建一个对象池。在 ACP 中,我们通过 GenericObjectPool 以及 GenericObjectPoolConfig 来创建一个对象池。

    // 声明一个对象池
    private GenericObjectPool<ChannelSftp> sftpConnectPool;
    
    // 设置连接池配置
            GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
            poolConfig.setEvictionPolicyClassName("tech.shuyi.javacodechip.acp.SftpEvictionPolicy");
            poolConfig.setBlockWhenExhausted(true);
            poolConfig.setJmxEnabled(false);
            poolConfig.setMaxWaitMillis(1000 * 10);
            poolConfig.setTimeBetweenEvictionRunsMillis(60 * 1000);
            poolConfig.setMinEvictableIdleTimeMillis(20 * 1000);
            poolConfig.setTestWhileIdle(true);
            poolConfig.setTestOnReturn(true);
            poolConfig.setTestOnBorrow(true);
            poolConfig.setMaxTotal(3);
            // 设置抛弃策略
            AbandonedConfig abandonedConfig = new AbandonedConfig();
            abandonedConfig.setRemoveAbandonedOnMaintenance(true);
            abandonedConfig.setRemoveAbandonedOnBorrow(true);
            this.sftpConnectPool = new GenericObjectPool<>(sftpConnectFactory, poolConfig, abandonedConfig);
    

    在上面创建 SFTP 连接池的代码中,我们配置了一些线程池的参数以及设置了抛弃策略。抛弃策略是非常重要的,如果没有设置抛弃策略,那么会拿到失效的连接从而导致获取文件失败。抛弃策略是通过 poolConfig.setEvictionPolicyClassName 来设置的,我们这里设置的是 SftpEvictionPolicy 类,其代码内容如下:

    @Slf4j
    @Component
    public class SftpEvictionPolicy implements EvictionPolicy<com.jcraft.jsch.ChannelSftp> {
        @Override
        public boolean evict(EvictionConfig config, PooledObject<com.jcraft.jsch.ChannelSftp> underTest, int idleCount) {
            try {
                // 连接失效时进行驱逐
                if (!underTest.getObject().isConnected()) {
                    log.warn("connect time out, evict the connection. time={}",System.currentTimeMillis() - underTest.getLastReturnTime());
                    return true;
                }
            }catch (Exception e){
                return true;
            }
            return false;
        }
    }
    

    看到这里,创建线程池的代码就结束了,SftpConnectPool 文件的全部内容如下:

    @Slf4j
    public class SftpConnectPool {
    
        private GenericObjectPool<ChannelSftp> sftpConnectPool;
    
        public SftpConnectPool(SftpConnectFactory sftpConnectFactory) {
            // 设置连接池配置
            GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
            poolConfig.setEvictionPolicyClassName("tech.shuyi.javacodechip.acp.SftpEvictionPolicy");
            poolConfig.setBlockWhenExhausted(true);
            poolConfig.setJmxEnabled(false);
            poolConfig.setMaxWaitMillis(1000 * 10);
            poolConfig.setTimeBetweenEvictionRunsMillis(60 * 1000);
            poolConfig.setMinEvictableIdleTimeMillis(20 * 1000);
            poolConfig.setTestWhileIdle(true);
            poolConfig.setTestOnReturn(true);
            poolConfig.setTestOnBorrow(true);
            poolConfig.setMaxTotal(3);
            // 设置抛弃策略
            AbandonedConfig abandonedConfig = new AbandonedConfig();
            abandonedConfig.setRemoveAbandonedOnMaintenance(true);
            abandonedConfig.setRemoveAbandonedOnBorrow(true);
            this.sftpConnectPool = new GenericObjectPool<>(sftpConnectFactory, poolConfig, abandonedConfig);
        }
    
        public ChannelSftp borrowObject() {
            try {
                return sftpConnectPool.borrowObject();
            } catch (Exception e) {
                log.error("borrowObject error", e);
                return null;
            }
        }
    
        public void returnObject(ChannelSftp channelSftp) {
            if (channelSftp!=null) {
                sftpConnectPool.returnObject(channelSftp);
            }
        }
    }
    

    为了方便使用,我还增加了 borrowObject 和 returnObject 方法,但这两个并不是必须的。在这两个方法中,我们分别调用了 GenericObjectPool 类的 borrowObject 方法和 returnObject 方法。这正是 ACP 提供的、使用线程池对象的方法,先借一个对象,之后归还对象。

    注:其实在这一步,已经包含了对象池的使用了。但实际使用的时候,我们经常是将对象池的声明与使用放在同一个类中,因此为了讲解方便,这里没有分开。因此下文的使用对象池,本质上是对对象池做进一步封装。

    使用对象池

    到这里我们的 SFTP 对象池就已经创建完毕了,是不是非常简单呢!但在实际的工作中,我们通常会在这基础上,做一些封装。对于我们这次的 SFTP 连接池来说,我们会对外直接提供下载文件的服务,将 SFTP 对象池进一步封装起来,不需要关心怎么获取文件。

    public class SftpFileHelper {
    
        @Autowired
        private SftpConnectPool sftpConnectPool;
    
        public void download(String dir, String file, String saveUrl)throws IOException {
            ChannelSftp sftp = sftpConnectPool.borrowObject();
            log.info("begin to download file, dir={}, file={}, saveUrl={}", dir, file, saveUrl);
            try {
                if (!StringUtils.isEmpty(dir)) {
                    sftp.cd(dir);
                }
                File downloadFile = new File(saveUrl);
                sftp.get(file, new FileOutputStream(downloadFile));
            }catch (Exception e){
                log.warn("下载文件失败", e);
            }finally {
                sftpConnectPool.returnObject(sftp);
            }
            log.info("file:{} is download successful", file);
        }
    }
    

    最后我们写一个测试用例来试一试,是否能正常下载文件。

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @Slf4j
    public class SftpFileHelperTest {
    
        @Autowired
        private SftpFileHelper sftpFileHelper;
    
        @Test
        public void testDownloadFtpFile() throws Exception {
            sftpFileHelper.download("dir", "fileName", "fileName");
        }
    }
    

    没有意外的话,你会看到一条绿线,文件已经被成功下载了!

    文章首发于公众号「架构师指南」及个人博客 shuyi.tech,欢迎关注访问。

    总结

    本文针对 Apache Commons Pool 库最常用的对象池功能做了演示。看完这篇文章,我们知道创建一个线程池需要三个步骤,分别是:

    • 创建对象工厂:告诉 ACP 如何创建你要的对象。
    • 创建对象池:告诉 ACP 你想创建一个怎样的对象池、设置驱逐策略。
    • 使用对象池:ACP 告诉你如何使用你的对象。

    本文相关代码存放在博主 Github 项目:java-code-chip 中,可以点击地址获取:java-code-chip/src/main/java/tech/shuyi/javacodechip/acp at master · chenyurong/java-code-chip

    ACP 库能够让读者朋友们快速地创建一个对象池,更加专注于业务内容。但事实上,ACP 提供的内容远不止如此,它还有更多更高级的功能。

    例如当我们连接的 SFTP 服务器有多个时,我们需要通过不同地址来获得不同的连接对象。此时最笨的办法是每个不同的地址,都复制多一份代码,然后通过不同类的不同方法来实现。但这样的情况工作量相当可观,并且也会有很多重复代码。这种时候就可以使用BaseKeyedPooledObjectFactory 来替代 BasePooledObjectFactory,从而实现通过 key 来实现不同地址的连接对象管理。

    更多关于 ACP 的内容,感兴趣的同学可以自行探索,这里就不深入讲解了。

    谢谢大家的阅读。如果文章对你有帮助,点个「点赞」 ,或者分享到朋友圈 吧。

    文章首发于公众号「陈树义」及个人博客 shuyi.tech,欢迎关注访问。

    参考资料

    扫描关注微信公众号
  • 相关阅读:
    VOA 2009/11/02 DEVELOPMENT REPORT In Kenya, a Better Life Through Mobile Money
    2009.11.26教育报道在美留学生数量创历史新高
    Java中如何实现Tree的数据结构算法
    The Python Tutorial
    VOA HEALTH REPORT Debate Over New Guidelines for Breast Cancer Screening
    VOA ECONOMICS REPORT Nearly Half of US Jobs Now Held by Women
    VOA ECONOMICS REPORT Junior Achievement Marks 90 Years of Business Education
    VOA 2009/11/07 IN THE NEWS A Second Term for Karzai; US Jobless Rate at 10.2%
    Ant入门
    Python 与系统管理
  • 原文地址:https://www.cnblogs.com/chanshuyi/p/how-to-impl-an-connect-pool-quickly.html
Copyright © 2011-2022 走看看