Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

连接池线程池

2017-10-28
nivelle

定义

应用系统开发过程中,我们常会用到池化技术,如对象池,连接池,线程池等.通过池化来减少一些消耗,以提升性能.对象池通过复用对象从而减少创建对象,垃圾回收的开销,但是,池不能太大,太大会影响GC时的扫描时间.连接池如数据库连接池,Redis连接池,HTTP连接池,通过复用TCP连接来减少创建和释放连接的时间来提升性能.线程池也是类似的,通过复用TCP连接来减少创建和释放连接的时间来提升性能.线程池也是类似的,通过复用线程提升性能.也就是说池化的目的就是通过复用技术提升性能.

数据库连接池

数据库连接池有很多实现,如C3P0,DBCP,Druid等.

  • DBCP连接池配置
 <bean id="dataSource"  
          class="org.apache.commons.dbcp.BasicDataSource"  
          destroy-method="close">  
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
        <property name="url" value="jdbc:mysql://192.168.0.109:3306/test?useUnicode=true&characterEncoding=UTF-8"/>  
        <property name="username" value="root"/>  
        <property name="password" value="root"/>  
        <!--maxActive: 最大连接数量-->    
        <property name="maxActive" value="150"/>  
        <!--minIdle: 最小空闲连接-->    
        <property name="minIdle" value="5"/>  
        <!--maxIdle: 最大空闲连接-->    
        <property name="maxIdle" value="20"/>  
        <!--initialSize: 初始化连接-->    
        <property name="initialSize" value="30"/>  
        <!-- 连接被泄露时是否打印 -->  
        <property name="logAbandoned" value="true"/>  
        <!--removeAbandoned: 是否自动回收超时连接-->    
        <property name="removeAbandoned"  value="true"/>  
        <!--removeAbandonedTimeout: 超时时间(以秒数为单位)-->    
        <property name="removeAbandonedTimeout" value="10"/>  
        <!--maxWait: 超时等待时间以毫秒为单位 1000等于60秒-->  
        <property name="maxWait" value="1000"/>  
        <!-- 在空闲连接回收器线程运行期间休眠的时间值,以毫秒为单位. -->  
        <property name="timeBetweenEvictionRunsMillis" value="10000"/>  
        <!--  在每次空闲连接回收器线程(如果有)运行时检查的连接数量 -->  
        <property name="numTestsPerEvictionRun" value="10"/>  
        <!-- 1000 * 60 * 30  连接在池中保持空闲而不被空闲连接回收器线程-->  
        <property name="minEvictableIdleTimeMillis" value="10000"/>  
    <property name="validationQuery" value="SELECT NOW() FROM DUAL"/>  
    </bean>  

  1. 数据库连接配置

配置数据库连接URL,用户名,密码,Statement默认超时时间(defaultQueryTimeout),事物自动提交(defaultAutoCommit),数据库连接属性(connectionPropertities,配置如连接超时时间等数据库特有属性,也可以在JDBC URL后面通过”?prorName=propValue)配置connectionPropertites.

  1. 池配置

配置连接池队列类型(lifo)是采用LIFO还是FIFP获取连接,默认FIFP.

其配置项包括初始大小(initialSize),最小空闲大小(minIdle),最大空闲大小(maxIdle),最大大小(maxTotal).连接如果不使用则会进入空闲,因此,空闲连接可以根据实际情况保持存活.如交易系统基本上不停工作,可以考虑将如上几个配置设置为一样,减少过期操作.

还有一个maxWaitMillis,用于配置当数据库连接池没可用连接时的最大等待时间,当超时候将抛出异常.

  1. 验证数据库连接有效性

三种方法:

  • 主动测试:创建连接时,获取连接时,释放连接时 testOnBorrow是获取连接时测试,testOnCreate是创建连接时测试,testOnReturn是释放连接时测试,测试代码如下:
public boolean validateObject(PooledObject<PoolableConnection> P) {
        try {
            validateLifetime(p);
            validateConnection(p.getObject());
            return true;
        } catch (Exception e) {
            return false;//表明当前连接要释放/销毁
        }

    }

    //验证连接的最大生存时间,如果配置了而且超出了最大生存期,F抛出异常
    private void validateLifetime(PooledObject<PoolableCOnnection> p) throws Exception {
        if (maxConnLifetimeMillis > 0) {
            long lifetime = System.currentTimeMillis() - p.getCreatedTime();
            if (lifetime > maxConnLifetimeMillis) {
                throw new LifetimeExceededException(Utils.getMessage(
                        "connectionFactory.lifetimeExceeded", Long.valueOf(lifetime), Long.valueOf(maxConnLifetimeMillis)
                ));
            }
        }
    }

    //发送验证命令给底层数据库验证存活,validationQuery要配置为至少返回一行记录的select语句;
    //如果不配置,则默认使用connection#isValid(int timeout)测试
    public void validateConnection(PoolableConection conn) throws SQLException{
        if(conn.isClosed()){
            throw  new SQLException("validateConnection:connection closed");
        }
        conn.validate(_validationQuery,_validationQuerTimeout);
    }


  • 定时测试:通过定时器定期测试,关闭孤儿连接

    如果获取连接后一直没有释放回池中,即该连接泄露了,如果不关闭的话,则会造成数据连接被用完,因此,可以考虑配置removeAbandoned* 进行关闭孤儿连接.不过不建议配置,把代码写健壮.

  • 使用配置validationQuery(如果不配置默认调用Connection#isValid进行验证)和maxConLifetimeMillis(连接生存的最长时间)来验证释放可用

DBCP配置建议

如果并发量大则建议:几个池大小设置为一样,禁用关闭孤儿连接,禁用定时器 .

如果并发量不大则建议:可以按需设置池大小,禁用关闭孤儿连接,启用定时器(MySql空闲连接8小时自动断开)

不管使用哪种方式记得设置超时时间,在JVM关闭/重启时一定要销毁连接池(bean配置destroy-method=”close”),因此如果没有加destroy-method,而且重启次数太频繁,将造成重启tomcat后旧的数据库连接池连接不释放,这样会有很多数据库连接在一段时间内不释放,造成启动后无法建立连接.

连接池使用的一些建议

一是要注意网络阻塞/不稳定时的级联效应.连接池内部应该根据当前网络状态(比如超时次数太多),对于一定时间内的全部timeout,根本不进行await(maxWait),即有熔断和快速失败机制.

当前等待连接池的人数,比如现在等待1000个,那么接下来的等待时没有意义的,这样还会造成滚雪球效应.

等待超时应该尽可能小点,即使返回错误页,也比等待并且阻塞强.DBCP比较容易出的问题是设置超时时间太长,造成大量TIMED_WAIT和线程阻塞,而且像滚雪球,一旦出问题很难立即恢复.

线程池

线程池的目的类似于连接池,通过减少频繁创建和销毁线程来降低新能损耗.每个线程都需要一个内存栈,用来存储如局部变量,操作栈等信息,可以通过-Xss参数来调整每个线程栈大小(64位系统默认1024KB,可以根据实际情况调小,比如256kb),通过调整该参数可以创建更多的线程,不过jvm不能无限地创建线程,通过使用线程池可以限制创建的线程数,从而保护系统.

线程池一般配合队列一起工作,使用线程池限制并发处理任务的数量,然后设置队列的大小,当任务超过队列大小时,通过一定的拒绝策略来处理,这样可以保护系统免受大流量儿导致崩溃—只是部分拒绝服务,还是有一部分是可以正常服务的.

线程池一般有核心线程池大小和线程池最大大小配置,当线程池中的线程空闲一段时间会被回收,而核心线程池中的线程不会被回收.

线程池大小多少合适,根据实际业务来压测决定,或者根据利特儿法则来计算出一个合理的线程池大小,定义是,在一个稳定的系统中,长时间观察到的平均用户数量L,等于长时间观察到的有效到达速率v与平均每个用户再系统中花费的时间的乘积.即L=vW.但实际情况是复杂的,如存在处理超时,网络抖动都会导致线程花费时间不一样.因此,还要考虑超时机制,线程隔离机制,快速失败机制等.用来保护系统免遭大量请求或异常情况的冲击.

java 提供了ExecutorService的三种实现:

  • ThreadPoolExecutor:标准线程池
  • ScheduledThreadPoolExecutor:支持延迟任务的线程池
  • ForkJoinPool:类似于ThreadPoolExecutor,但是使用work-stealing模式,其会为线程池中的每个线程创建一个队列,从而work-stealing(任务窃取)算法使得线程可以从其他线程池队列里窃取任务来执行.即如果自己的任务处理完了,则可以从其他线程哪里窃取任务执行.

Java线程池

使用Executors来创建线程池.

  1. 创建单线程的线程池.
ExecutorService executorService = Executors.newSingleThreadExecutor();

等价于

return new FinalizableDelegatedExecutorsService(new ThreadPoolExecutor(1,1,0l, TimeUnit.MICROSECONDS,

new LinkedBlockingQueue<Runnable>()));
  1. 创建固定数量的线程池
ExecutorService executorService1 Executors.newFixedThreadPool(10);

等价于
return new ThreadPoolExecutor(nThreads,nThreads,0l,TimeUnit.MICROSECONDS,new LinkedBlockingQueue<Runnable>());
  1. 创建可缓存的线程池,初始化大小为0,线程池最大大小为Inter.MAX-VALUE.其使用SynchronousQueue队列,一个没有数据缓冲的阻塞队列.对其执行put操作后必须等待take操作消费该数据,反之亦然.该线程池不限制最大大小,如果线程池有空闲,则复用,否则创建新线程.如果线程池中的线程空闲60秒,则将被回收.该线程默认最大大小为Integer.MAX_VALUE,请确认必要后再使用该线程池
ExecutorService executorService2 Executors.newCachedThreadPool();
        
等价于 return ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>())

  1. 支持延迟执行的线程池,其使用DelayedWorkQueue实现任务延迟.

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        
等价于 return new ScheduledThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE,0,NANOSECPNDS,new DelayQueue<>());

  1. work-stealing 线程池,默认并行数为Runtime.getRuntime().avaliableProcessors()
ExecutorService executorService= Executors.newWorkStealingPool(5);

等价于 return new ForkJoinPool(parallelism,ForkJoinPool.defaultForkJoinWorkerThreadFactory(),null,true);

ThreadPoolExecutor配置:

  • corePoolSize:核心线程池大小,线程池维护的线程最小大小,即没有任务处理情况下,线程池可以有多个空闲线程,类似于DBCP的minIdle
  • maximumPoolSize:线程池最大大小,当任务书非常多时,线程池可以创建的最大线程数量
  • keepAliveTime:线程池中线程的最大空闲时间,存活时间超过该时间的线程会被回收,线程池会一直缩小到corePoolSize大小
  • workQueue:线程池使用的任务缓冲队列包括阻塞数组队列ArrayBlockingQueue,有界/无界阻塞链表队列LinkedBlockingQueue,优先级阻塞队列PriorityBlockingQueue,无缓冲区阻塞队列synchronousQueue.有界阻塞队列必须要设置合理的队列大小
  • threadFactory:创建线程的工厂,我们可以设置线程的名字,是否是后台线程
  • rejectedExecutionHandler:当缓存队列满后的拒绝策略,包括Abort(直接抛出RejectedExecutionException),Discard(按照LIFO丢弃),DiscardOldest(按照LRU丢弃),CallsRun(主线程执行)

线程池终止

线程池不再使用后记得停止掉,可以调用shutdown以确保不接受新任务,并等待线程池中任务处理完成后再退出,或调用shutdownNow清除未执行的任务,并用Thread.interrupt停止正在执行的任务.然后调用awaitTermination方法等待终止操作执行完成,代码如下:

Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run(){
                executorService.shutdown();
                executorService.awaitTermination(30,TimeUnit.SECONDS);
            }
        });


上一篇 限流

下一篇 原生类型

评论