场景1:数据库连接池(IO密集型) 如果每次数据库请求都请求都开启一个连接,然后用完销毁,性能会有问题。
分析程序的日志之后,你发现系统慢的原因出现在和数据库的交互上。因为你们数据库的调用方式是先获取数据库的连接,然后依靠这条连接从数据库中查询数据,最后关闭连接释放数据库资源。
我们统计了一段时间的 SQL 执行时间,发现 SQL 的平均执行时间大概是 1ms,也就是说相比于 SQL 的执行,MySQL 建立连接的过程是比较耗时的。
连接池设计 数据库连接池有两个最重要的配置:最小连接数和最大连接数 。
如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
如果连接池中有空闲连接则复用空闲连接;
如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配置是 checkoutTimeout)等待旧的连接可用;
如果等待超过了这个设定时间则向用户抛出错误。
特别注意第一条,当所有连接小于最小连接时,优先创建连接,而不是先去空闲池中获取连接。
对于数据库连接池,根据我的经验,一般在线上我建议最小连接数控制在 10 左右,最大连接数控制在 20~30 左右即可。
除此之外,连接放在池中,分为空闲连接池和忙碌连接池:
空闲连接池:其中的连接,可以直接取出,并返回给开发者
忙碌连接池:从空闲连接池取出的就放入忙碌连接池
要检测mysql连接的状态,原因有是:MySQL 有个参数是“wait_timeout”,控制着当数据库连接闲置多长时间后,数据库会主动地关闭这条连接。这个机制对于数据库使用方是无感知的,所以当我们使用这个被关闭的连接时就会发生错误。
检测方法:启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”的命令给数据库看是否会抛出异常 ,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用,也是我比较推荐的方式。
代码实现 类的定义:
1 2 3 4 5 6 7 8 9 10 11 class DbConnectPool { constructor ( ) { this .minCount = 10 ; this .maxCount = 30 ; this .freePool = []; this .busyPool = []; this .checker = null ; } }
获取连接和回收连接逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 async createConnect ( ) {}async getConnect (retryTimes = 0 ) { if (retryTimes > 2 ) { throw new Error ('暂无可用数据库连接' ) } if (this .freePool .length + this .busyPool .length < this .minCount ) { const n = await this .createConnect (); this .busyPool .push (n); return n; } if (this .freePool .length > 0 ) { const top = this .pool .pop (); this .busyPool .push (top); return top; } if (this .freePool .length + this .busyPool .length < this .maxCount ) { const n = await this .createConnect (); this .busyPool .push (n); return n; } await sleep (10 ); return await this .getConnect (retryTimes + 1 ) } recycleConnect (connect ) { const index = this .busyPool .findIndex (item => item === connect) if (index === -1 ) { return ; } this .busyPool .splice (index, 1 ); this .freePool .push (connect); }
心跳策略检查连接可用性逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 startCheck ( ) { if (this .checker ) { return ; } this .checker = setInterval (() => { this .checkConnect (this .busyPool , 'busy' ); this .checkConnect (this .freePool , 'free' ) }, 100 ) } async checkConnect (allConnect, mode = '' ) { const validConnect = [] for (let i = 0 ; i < allConnect; ++i) { try { await connect.send ('SELECT 1' ); validConnect.push (allConnect[i]) } catch (error) { console .log ('......' ) } } if (mode === 'busy' ) { this .busyPool = validConnect; } else if (mode === 'free' ) { this .freePool = validConnect; } }
销毁逻辑,防止内存泄漏:
1 2 3 4 5 6 7 8 9 10 async destory ( ) { const allConnect = [...this .busyPool , ...this .freePool ]; for (const connect of allConnect) { await connect.close (); } this .busyPool .length = 0 ; this .freePool .length = 0 ; clearInterval (this .checker ); }
场景2:计算线程池(计算密集型) 设计 JDK 1.5 中引入的 ThreadPoolExecutor 就是一种线程池的实现。
逻辑:
如果线程池中的线程数少于 coreThreadCount 时,处理新的任务时会创建新的线程;
如果线程数大于 coreThreadCount 则把任务丢到一个队列里面,由当前空闲的线程执行;
当队列中的任务堆积满了的时候,则继续创建线程,直到达到 maxThreadCount;
当线程数达到 maxTheadCount 时还有新的任务提交,那么我们就不得不将它们丢弃了。
为什么超过minCount(coreCount)后,先插入队列,而不是像连接池直接创建?
JDK 实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程,它比较适用于执行 CPU 密集型的任务,也就是需要执行大量 CPU 运算的任务。 因为执行 CPU 密集型的任务时 CPU 比较繁忙,因此只需要创建和 CPU 核数相当的线程就好了,多了反而会造成线程上下文切换,降低任务执行效率。所以当前线程数超过核心线程数时,线程池不会增加线程,而是放在队列里等待核心线程空闲下来。
针对IO密集型操作,比如缓存查询、数据库查询,在执行 IO 操作的时候 CPU 就空闲了下来,这时如果增加执行任务的线程数而不是把任务暂存在队列中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞吐量。