数据库链接(2) - 为何C3P0链接池那么慢

摘要

承接上篇数据库链接(1)从jdbc到mybatis,介绍下数据库链接池技术java

为何须要链接池

在上一篇中咱们介绍说客户端创建一次链接耗时太长(创建链接,设置字符集,autocommit等),若是在每一个sql操做都须要经历创建链接,关闭链接。不只应用程序响应慢,并且会产生不少临时对象,应用服务器GC压力大。另外数据库server端对链接也有限制,好比MySQL默认151个链接(实际环境中通常会调大这个值,尤为是多个服务时)node

如今面临的问题就是如何提升对稀缺性的资源高效管理。由于客户端与数据库的链接本质就是tcp请求,加上基于tcp协议封装的mysql请求。那么一般解决这类问题,咱们有两种方式,一种是池话技术,即便用一个容器,提早建立好链接,请求时直接从池子里面拿,另一种就是利用IO多路复用技术。利于在spring5中,mongo ,cassandra等数据库的访问就能够利用reactive来实现了,可是关系型数据库不行,缘由在于关型数据库的访问目前都是基于JDBC,JDBC操做数据库的流程,创建connection,构建Statement,执行这一套是串行的,阻塞型。一个事务中的多个操做只能在同一个链接中完成。因此不能使用IO多路复用技术,是受限于JDBC的阻塞。对于其余语言,是能够的,好比nodejsmysql

因此咱们使用池话技术来提供数据库访问react

数据库链接池与线程池的区别

一般,程序员在业务开发中常用的是线程池,利用CPU多核,来并发处理任务,提升效率。数据库链接池与线程池同属于池化技术,没有太大区别,都是须要管理池的大小,资源控制。不一样的数据库链接池中放的是connection,同时还须要管理事务,因此一般数据库链接池中会对这个进行优化git

从链接池中取链接执行sql操做,多了两步设置connection autocommit属性操做 程序员

这里写图片描述

经过将connection分红两组,来提供效率 github

这里写图片描述

开源链接池技术介绍

这里写图片描述

一个基本的数据库链接池包括几大部分spring

  • 取出链接sql

  • 放回链接数据库

  • 异步/同步处理线程

    进行建立链接和销毁链接 对于一个数据库链接池的根本就在于并发容器的实现,也是决定链接池的效率高低,常见的链接池配置以下

initialSize:初始链接数
maxActive: 最大链接数量
minIdle: 最小链接数量
maxWait: 获取链接最大等待时间ms
minEvictableIdleTimeMillis:链接保持空闲而不被驱逐的最小时间
timeBetweenEvictionRunsMillis:销毁线程的时间检测
testOnBorrow:申请链接时执行,比较影响性能
validationQuery:testOnBorrow为true检测是不是有效链接sql
testWhileIdle:申请链接的时候检测

复制代码

目前的开源数据库链接池主要有如下,

这里写图片描述

C3P0,和DBCP是出现的比较早的数据库链接,主要用于hibernate,和tomcat6.0如下,比较稳定,在低并发的状况下,工做还能够,可是高并发下,性能比较差,因此在tomcat6,又重写了一个jdbc-pool,来替代DBCP。

Druid是阿里巴巴开源的高性能数据库链接池,目前基本是各大互联网公司的标配了,加上又是国内的,文档比较易读,因此流行度比较高,另一个是hikariCP,性能比较高,目前普及度还不是特别高。

那为何C3P0和DBCP的性能比较低呢? 前面提到数据库链接池本质上就是一个并发容器的实现。一般咱们能够利用List+锁机制实现。或者使用jdk原生的,好比CopyOnWriteList这样的结构 而锁经过有两种,一种JVM级别的synchronized,一种是JDK提供的ReentrantLock,二者在语义上并无多大区别,互斥,内存可见,可重入。JDK5中引入ReentrantLock时,性能比synchronzied要好不少,而在JDK6中,通过优化后的,二者并没有太大性能上区别。因此ReentrantLock更多优点在于

  • 能够中断等待的线程 一直拿不到锁的等待线程,能够中断掉,避免出现死锁

  • 能够结合Condition,更加灵活控制线程

看下com.mchange.v2.c3p0.DriverManagerDataSource 的实现

// should NOT be sync'ed -- driver() is sync'ed and that's enough // sync'ing the method creates the danger that one freeze on connect
    // blocks access to the entire DataSource
    public Connection getConnection() throws SQLException
    { 
        ensureDriverLoaded();
        // 经过此方法来获取链接
        Connection out = driver().connect( jdbcUrl, properties ); 
        if (out == null)
            throw new SQLException("Apparently, jdbc URL '" + jdbcUrl + "' is not valid for the underlying " +
                            "driver [" + driver() + "].");
        return out;
    }
复制代码

在获取链接的时候首先在一个synchonized中去获取java.sql.Driver,

private synchronized Driver driver() throws SQLException
    {
 	//To simulate an unreliable DataSource...
   	//double d = Math.random() * 10;
   	//if ( d > 1 )
   	//    throw new SQLException(this.getClass().getName() + " TEST of unreliable Connection. If you're not testing, you shouldn't be seeing this!");

        //System.err.println( "driver() <-- " + this );
        if (driver == null)
	{
	    if (driverClass != null && forceUseNamedDriverClass)
	    {
		if ( Debug.DEBUG && logger.isLoggable( MLevel.FINER ) )
		    logger.finer( "Circumventing DriverManager and instantiating driver class '" + driverClass + 
				  "' directly. (forceUseNamedDriverClass = " + forceUseNamedDriverClass + ")" );

		try 
		{ 
		    driver = (Driver) Class.forName( driverClass ).newInstance();
		    this.setDriverClassLoaded( true );
		}
		catch (Exception e)
		    { SqlUtils.toSQLException("Cannot instantiate specified JDBC driver. Exception while initializing named, forced-to-use driver class'" + driverClass +"'", e); }
	    }
	    else
		driver = DriverManager.getDriver( jdbcUrl );
        }
        return driver;
    }
复制代码

具体的链接池管理是BasicResourcePool,能够看下代码,里面全都是synchronized方法。并发性能怎么能好。

再来看下Druid的实现,DruidDataSource

private DruidPooledConnection getConnectionInternal(long maxWait) throws SQLException {
        DruidConnectionHolder holder;

        for (boolean createDirect = false;;) {
                // 带有超时的链接获取
                if (maxWait > 0) {
                    holder = pollLast(nanos);
                } else {
                    holder = takeLast();
                }
    }
复制代码

并发环境下去拿链接时,并无在读操做上加锁,比互斥锁的性能要高 互斥锁是一种比较保守的策略,像synchronized,它避免了写写冲突,写读冲突,和读读冲突,对于数据库链接池,应用程序来拿,是一个读操做比较多的,容许多个读同时操做,可以提升系统的并发性。

private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException {
        long estimate = nanos;

		// 队列阻塞,当取链接时,没有链接,线程空转,等待另外建立线程去建立链接
        for (;;) {
            if (poolingCount == 0) {
             // 通知建立线程去建立链接
             emptySignal(); 
             }
	   }
       decrementPoolingCount();
        // 从数组中获取链接
        DruidConnectionHolder last = connections[poolingCount];
        connections[poolingCount] = null;

        long waitNanos = nanos - estimate;
        last.setLastNotEmptyWaitNanos(waitNanos);

        return last; 
    }

复制代码

在建立链接线程,销毁链接线程中增长写锁

private boolean put(DruidConnectionHolder holder) {
		// 加锁
        lock.lock();
        try {
            if (poolingCount >= maxActive) {
                return false;
            }
            connections[poolingCount] = holder;
            incrementPoolingCount();

            if (poolingCount > poolingPeak) {
                poolingPeak = poolingCount;
                poolingPeakTime = System.currentTimeMillis();
            }
            //发出链接池非空信号,等待的线程开始处理
            notEmpty.signal();
            notEmptySignalCount++;

            if (createScheduler != null) {
                createTaskCount--;

                if (poolingCount + createTaskCount < notEmptyWaitThreadCount //
                    && activeCount + poolingCount + createTaskCount < maxActive) {
                    emptySignal();
                }
            }
        } finally {
            lock.unlock();
        }
        return true;
    }
复制代码

HikariCP在读写锁的基础上进行了进一步的优化 github.com/brettwooldr…

关注【方丈的寺院】,与方丈一块儿开始技术修行之路

在这里插入图片描述

参考

my.oschina.net/javahongxi/…

相关文章
相关标签/搜索