首页 > 代码库 > 【转帖】置高并发jdbc连接池
【转帖】置高并发jdbc连接池
简单的MySQL连接池
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- />
当tomcat读到type="javax.sql.DataSource"属性时会自动重新安装DBCP,除非你指定不同的factory。factory object 本身就是创建和配置连接池的。
在Apache Tomcat中有两种方式配置 Resource elements
配置全局连接池
编辑conf/server.xml
- <GlobalNamingResources>
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- />
- </GlobalNamingResources>
然后你需要创建一个ResourceLink element使这个连接池对于web应用是可用的。如果你想要用同一个名字让连接池对于所有的应用有效,最简单的方法就是编辑conf/context.xml文件
- <Context>
- <ResourceLink type="javax.sql.DataSource"
- name="jdbc/LocalTestDB"
- global="jdbc/TestDB"
- />
- <Context>
注意,如果你不想要全局的连接池,可以从server.xml移除Resource element到你的web应用的context.xml 文件。
然后从刚配置好的连接池中获得连接,简单java代码:
- Context initContext = new InitialContext();
- Context envContext = (Context)initContext.lookup("java:/comp/env");
- DataSource datasource = (DataSource)envContext.lookup("jdbc/LocalTestDB");
- Connection con = datasource.getConnection();
使用java很简单
还可以使用Java syntax
- DataSource ds = new DataSource();
- ds.setDriverClassName("com.mysql.jdbc.Driver");
- ds.setUrl("jdbc:mysql://localhost:3306/mysql");
- ds.setUsername("root");
- ds.setPassword("password");
- PoolProperties pp = new PoolProperties();
- pp.setDriverClassName("com.mysql.jdbc.Driver");
- pp.setUrl("jdbc:mysql://localhost:3306/mysql");
- pp.setUsername("root");
- pp.setPassword("password");
- DataSource ds = new DataSource(pp);
设置连接池
我们将使用下面这些属性设置连接池
- initialSize
- maxActive
- maxIdle
- minIdle
去了解这些属性是很重要的,它们看起来很明显但又有一些神秘
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- initialSize="10"
- maxActive="100"
- maxIdle="50"
- minIdle="10"
- />
initialSize=10 设置连接池建立时连接的数目
- 当连接池定义在GlobalNamingResources中,连接池在Tomcat启动时创键
- 当连接池定义在Context中,连接池在第一次查找JNDI时创建
maxActive=100 连接数据库的最大连接数。这个属性用来限制连接池中能够打开连接的数量,可以方便数据库做连接容量规划。
minIdle=10 连接池中存在的最小连接数目。连接池中连接数目可以变很少,如果使用了maxAge属性,有些空闲的连接会被关闭因为离它最近一次连接的时间过去太久了。但是,我们看到的打开的连接不会少于minIdle。
maxIdle属性有一点麻烦。它的不同的行为取决于是否使用了pool sweeper。pool sweeper是一个可以在连接池正在使用的时候测试空闲连接和重置连接池大小的后台线程。还负责检测连接泄露。 pool sweeper 通过如下方式定义的:
- public boolean isPoolSweeperEnabled() {
- boolean timer = getTimeBetweenEvictionRunsMillis()>0;
- boolean result = timer && (isRemoveAbandoned() && getRemoveAbandonedTimeout()>0);
- result = result || (timer && getSuspectTimeout()>0);
- result = result || (timer && isTestWhileIdle() && getValidationQuery()!=null);
- return result;
- }
sweeper每timeBetweenEvictionRunsMillis milliseconds运行一次。
maxIdle定义如下
- Pool sweeper关闭,如果空闲连接池大于maxIdle,返回的连接将被关闭。
- Pool sweeper开启,空闲的连接数可以超过maxIdle,但如果连接空闲的时间已经超过minEvictableIdleTimeMillis,能缩小到minIdle。听起来很奇怪连接池为什么不关闭连接当空闲连接数量大于maxIdle。想想下面的情况:
- 100个线程处理100个并发请求
- 在一个请求中每个线程请求一个连接3次
在这种场景下,如果我们设置maxIdle=50,那么我们会关闭和打开50*3的连接数。这样增加了数据库的负重并且减慢了应用的速度。当达到连接高峰时,我们希望能够充分利用连接池中的所有连接。因此,我们强烈希望打开pool sweeper 。我们将在下一个部分探讨具体的事项。我们在这里额外说明maxAge这个属性。maxAge定义连接能够打开或者存在的时间,单位为毫秒。当一个连接返回到了连接池,如果这个连接已经使用过,并且距离它第一次被使用的时间大于maxAge时,这个连接会被关闭。
正如我们所看到的 isPoolSweeper算法实现,sweeper 将会被打开,当以下任一条件满足时
- timeBetweenEvictionRunsMillis>0 AND removeAbandoned=true ANDremoveAbandonedTimeout>0
- timeBetweenEvictionRunsMillis>0 AND suspectTimeout>0
- timeBetweenEvictionRunsMillis>0 AND testWhileIdle=true AND validationQuery!=null
As of version 1.0.9 the following condition has been added
- timeBetweenEvictionRunsMillis>0 AND minEvictableIdleTimeMillis>0
(timer && getMinEvictableIdleTimeMillis()>0);
因此设置最理想的连接池,我们最好修改我们的配置满足这些条件
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- initialSize="10"
- maxActive="100"
- maxIdle="50"
- minIdle="10"
- suspectTimeout="60"
- timeBetweenEvictionRunsMillis="30000"
- minEvictableIdleTimeMillis="60000"
- />
有效的连接
数据库连接池提出了一个挑战,因为连接池中的连接会过时。这是常有的事,要么数据库,或者可能是连接池和数据库中的一个设备,连接超时。唯一确定会话连接是活跃的真正办法是使连接在服务器和数据库做一个来回访问。在Java 6中,JDBC API处理验证连接是否是有效的方法是通过提供isValid变量来调用java.sql.Connection接口。在此之前,连接池不得不采用执行一个查询的方法,比如在MySQL上执行SELECT 1.数据库分析这句查询很简单,不需要任何的磁盘访问。isValid被计划实施,但 Apache Tomcat 6的连接池,也必须保存对Java 5的兼容性。
校验查询
校验查询会有一些挑战
- 如果它们频繁使用,会降低系统的性能
- 如果使用的间隔太久,会导致连接失效
- 如果应用调用setTransactionIsolation并设置autoCommit=false,如果应用再次调用setTransactionIsolation,会产生一个SQLException异常,因为校验查询可能在数据库中已经产生了一个新的transaction。
让我们看看最典型的配置:
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- testOnBorrow="true"
- validationQuery="SELECT 1"
- />
在这个配置中,java代码每次调用 Connection con = dataSource.getConnection()时都会执行一次SELECT 1查询。
这样保证了在连接提交给应用之前都已经测试过了。但是,对于在短时间内频繁使用连接的应用,会对性能有严重的影响。这有两个其他的配置选项:
- testWhileIdle
- testOnReturn
当在错误的时间对连接做测试,它们也不是真正的很有帮助。
对于很多应用来说,没有校验不是一个真正的困难。一些应用可以绕过校验通过设置minIdle=0和给minEvictableIdleTimeMillis一个很小的值,所以如果连接空闲了足够长的时间会让数据库会话超时,在此之前连接池将会移除这些空闲太久的连接。
最好的解决办法就是测试那些有一段时间没被测试过的连接。
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- testOnBorrow="true"
- validationQuery="SELECT 1"
- validationInterval="30000"
- />
在这个配置中,连接校验的间隔不会超过30s。这是在性能和连接验证上的折中。正如前面提到的,如果我们想侥幸验证所有的连接,我们可以配置连接池中所有空闲连接超时。
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- timeBetweenEvictionRunsMillis="5000"
- minEvictableIdleTimeMillis="5000"
- minIdle="0"
- />
建立数据库客户会话
在一些案例中,当初始化一个新的数据库会话时需要执行一些任务。可能包括执行一个简单的SQL声明或者执行一个存储过程。
当你创建触发器时候,这是在数据库层面上的典型操作。
- create or replace trigger logon_alter_session after logon on database
- begin
- if sys_context(‘USERENV‘, ‘SESSION_USER‘) = ‘TEMP‘ then
- EXECUTE IMMEDIATE ‘alter session ....‘;
- end if;
- end;
- /
这将影响所有的用户,在后面这种情况下这是不够的,当创建一个新的会话的时候我们希望执行一个自定义查询。
- <Resource name="jdbc/TestDB" auth="Container"
- type="javax.sql.DataSource"
- description="Oracle Datasource"
- url="jdbc:oracle:thin:@//localhost:1521/orcl"
- driverClassName="oracle.jdbc.driver.OracleDriver"
- username="default_user"
- password="password"
- maxActive="100"
- validationQuery="select 1 from dual"
- validationInterval="30000"
- testOnBorrow="true"
- initSQL="ALTER SESSION SET NLS_DATE_FORMAT = ‘YYYY MM DD HH24:MI:SS‘"/>
initSQL会被存在的每一条连接执行。
连接池泄露和长时间运行的查询
连接池包含一些诊断操作。jdbc-pool和Common DBCP都能够检测和减轻没有返回连接池中的连接。这里演示是被称为抛出内存泄露的连接。
- Connection con = dataSource.getConnection();
- Statement st = con.createStatement();
- st.executeUpdate("insert into id(value) values (1‘); //SQLException here
- con.close();
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- maxActive="100"
- timeBetweenEvictionRunsMillis="30000"
- removeAbandoned="true"
- removeAbandonedTimeout="60"
- logAbandoned="true"
- />
- removeAbandoned-如果我们想检测内存泄露的连接,可以设置为true
- removeAbandonedTimeout-调用dataSource.getConnection开始到丢弃检测到泄露连接的时间(seconds)
- logAbandoned-如果想用log记录丢弃的连接,可以设置为true。当设置为true时,调用dataSource.getConnection 时会记录一个堆栈追踪,并且被打印出来当连接没有返回的时候。
但我们想要这种类型的诊断,当然有可以使用的例子。也可以运行批处理作业一次执行一个连接几分钟。我们该如何处理这些问题?
两个额外的选项已经被加入来支持这些工作
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- maxActive="100"
- timeBetweenEvictionRunsMillis="30000"
- removeAbandoned="true"
- removeAbandonedTimeout="60"
- logAbandoned="true"
- abandonWhenPercentageFull="50"
- />
- abandonWhenPercentageFull-一条连接必须满足临界值 removeAbandonedTimeout和打开连接的数量必须超过这个百分比。
使用这个属性可能会在一次错误判断中产生在其他地方已经被认为丢弃的连接。设置这个值为100时意味着连接数除非到了maxActive限制时,是不会被考虑丢弃的。这给连接池增加了一些灵活性,但是不会让批处理作业使用单独连接5分钟。在这种情况,我们想确定当我们检测到连接仍然被使用时,我们重置超时计时器,因此,连接不会被考虑丢弃。我们通过插入一个拦截器实现。
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- maxActive="100"
- timeBetweenEvictionRunsMillis="30000"
- removeAbandoned="true"
- removeAbandonedTimeout="60"
- logAbandoned="true"
- abandonWhenPercentageFull="50"
- jdbcInterceptors="ResetAbandonedTimer"
- />
拦截器在org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer中被指定完全限定名称,或者在org.apache.tomcat.jdbc.pool.interceptor包中使用短类名
每次准备语句或者执行一次查询,连接池中的计时器会被重置放弃计时器。因为如此,在5分钟的批处理作业中执行多次查询和更新,都不会超时。
这是你当然想知道的情形,但你不会想去kill或者回收连接,因为你不会知道会对你的系统产生什么影响。
- <Resource type="javax.sql.DataSource"
- name="jdbc/TestDB"
- factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- driverClassName="com.mysql.jdbc.Driver"
- url="jdbc:mysql://localhost:3306/mysql"
- username="mysql_user"
- password="mypassword123"
- maxActive="100"
- timeBetweenEvictionRunsMillis="30000"
- logAbandoned="true"
- suspectTimeout="60"
- jdbcInterceptors="ResetAbandonedTimer"
- />
suspectTimeout属性的工作方式与removeAbandonedTimeout 相似,除了不关闭连接,而只是简单的记录警告和发布一个JMX通知信息。通过这种方式,你可以在不用改变你系统行为的情况下发现泄漏或者长查询。
从其它的数据源形成连接池
到目前为止我们处理连接池连接的获得是通过java.sql.Driver接口。因此我们使用属性
- driverClassName
- url
然而,一些连接配置是使用 javax.sql.DataSource 甚至是javax.sql.XADataSource接口,因此我们需要支持这些配置选项。
使用java相对是很容易的。
- PoolProperties pp = new PoolProperties();
- pp.setDataSource(myOtherDataSource);
- DataSource ds = new DataSource(pp);
- Connection con = ds.getConnection();
- DataSource ds = new DataSource();
- ds.setDataSource(myOtherDataSource);
- Connection con = ds.getConnection();
在我们处理XA连接时很方便。
在XML配置中,jdbc-pool会使用org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory类,一个能够允许配置任何类型的命名资源的简单类。为了设置 Apache Derby XADataSource 我们可以创建了下面的代码
- <Resource factory="org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory"
- name="jdbc/DerbyXA1"
- type="org.apache.derby.jdbc.ClientXADataSource"
- databaseName="sample1"
- createDatabase="create"
- serverName="localhost"
- portNumber="1527"
- user="sample1"
- password="password"/>
这是一个简单的通过端口1527连接到网络上的相邻实例的XADataSource.
如果你想要从这个数据源形成XA连接池,我们可以在它后面建立这个连接池节点。
- <Resource factory="org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory"
- name="jdbc/DerbyXA1"
- type="org.apache.derby.jdbc.ClientXADataSource"
- databaseName="sample1"
- createDatabase="create"
- serverName="localhost"
- portNumber="1527"
- user="sample1"
- password="password"/>
- <Resource factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- dataSourceJNDI="DerbyXA1"<!--Links to the Derby XADataSource-->
- name="jdbc/TestDB1"
- auth="Container"
- type="javax.sql.XADataSource"
- testWhileIdle="true"
- testOnBorrow="true"
- testOnReturn="false"
- validationQuery="SELECT 1"
- validationInterval="30000"
- timeBetweenEvictionRunsMillis="5000"
- maxActive="100"
- minIdle="10"
- maxIdle="20"
- maxWait="10000"
- initialSize="10"
- removeAbandonedTimeout="60"
- removeAbandoned="true"
- logAbandoned="true"
- minEvictableIdleTimeMillis="30000"
- jmxEnabled="true"
- jdbcInterceptors="ConnectionState;StatementFinalizer;SlowQueryReportJmx(threshold=10000)"
- abandonWhenPercentageFull="75"/>
这里我们通过dataSourceJNDI=DerbyXA1属性链接这两个数据源。这两个数据源都不得不存在同一个命名空间,在我们的例子中,是jdbc命名空间。
目前JNDI通过DataSource.setDataSourceJNDI(...)查找不被支持,只能通过factory对象。
如果你加入一个
- javax.sql.DataSource对象-连接池将会调用 javax.sql.DataSource.getConnection()方法
- javax.sql.DataSource 对象但是在连接池中指定了username/password-连接池将会调用javax.sql.DataSource.getConnection(String username, String password) 方法
- javax.sql.XADataSource对象-连接池将会调用 javax.sql.XADataSource.getXAConnection() 方法
- javax.sql.XADataSource 对象但是在连接池中指定了 username/password-连接池将会调用javax.sql.DataSource.getXAConnection(String username, String password) 方法
这是一个有趣的现象当你处理 XADataSources。你可以把返回的对象转换为java.sql.Connection对象或者javax.sql.XAConnection对象,并且对同一个对象的两个接口调用方法。
- DataSource ds = new DataSource();
- ds.setDataSource(myOtherDataSource);
- Connection con = ds.getConnection();
- if (con instanceof XAConnection) {
- XAConnection xacon = (XAConnection)con;
- transactionManager.enlistResource(xacon.getXAResource());
- }
- Statement st = con.createStatement();
- ResultSet rs = st.executeQuery(SELECT 1);
JDBC 拦截器
JDBC 拦截器创建是为了实现灵活性。javax.sql.PooledConnection 从底层驱动封装了java.sql.Connection/javax.sql.XAConnection或者数据源本身就是一个拦截器。拦截器以java.lang.reflect.InvocationHandler接口为基础。拦截器是一个继承自org.apache.tomcat.pool.jdbc.JdbcInterceptor的类。
在本文中,我们将介绍如果配置拦截器。在我们下一篇文章,我们将介绍如果实现自定义拦截器和它们的生命周期。
- <Resource factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- ...
- jdbcInterceptors="ConnectionState;StatementFinalizer;SlowQueryReportJmx(threshold=10000)"
- />
与下面的相同
- <Resource factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
- ...
- jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;
- org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer;
- org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReportJmx(threshold=10000)"
- />
拦截器可以使用一个短小的名称,比如ConnectionState,如果这个拦截器定义在org.apache.tomcat.jdbc.pool.interceptor 包中。
否则,必须使用一个完全限定名称。
拦截器定义在以;分割的字符串中。拦截器可以在括号内定义0个或多个参数。参数是以逗号分割的简单键值对。
连接状态
java.sql.Connection接口有如下属性
- autoCommit
- readOnly
- transactionIsolation
- catalog
这些属性的默认值可以使用如下的内容为连接池配置
- defaultAutoCommit
- defaultReadOnly
- defaultTransactionIsolation
- defaultCatalog
如果设置了这些属性,当建立连接到数据库时配置这个连接。如果没有配置 ConnectionState拦截器,在建立连接时设置这些属性会是一次性操作。如果配置了ConnectionState拦截器,每次从连接池取出的连接会将被重置为期望的状态。
其中有些方法在执行查询时会导致往返数据库。比如,调用 Connection.getTransactionIsolation()会导致驱动查询当前会话的事务隔离级别。这种往返会导致严重的性能问题并影响应用在频繁的使用连接执行非常短和快的操作的时候。 ConnectionState 拦截器可以缓存这些操作的值并调用方法查询它们从而避免往返数据库。
Statement Finalizer
java代码在使用java.sql对象后需要清除和释放使用过的资源。
一个清理代码示例
- Connection con = null;
- Statement st = null;
- ResultSet rs = null;
- try {
- con = ds.getConnection();
- ...
- } finally {
- if (rs!=null) try { rs.close(); } catch (Exception ignore){}
- if (st!=null) try { st.close(); } catch (Exception ignore){}
- if (con!=null) try { con.close();} catch (Exception ignore){}
- }
当一个连接返回连接池的时候,StatementFinalizer拦截器确保 java.sql.Statement和它的子类正确关闭。
获得真正的JDBC连接
使用javax.sql.PooledConnection工具返回代理连接,因此取出连接十分直接,不需要转换为特殊的类。
同样适用于你配置了处理javax.sql.XAConnection的连接池。
另一个有趣的取出底层连接的方法是
- Connection con = ds.getConnection();
- ction underlyingconnection = con.createStatement().getConnection();
PS:翻译原文——http://www.tomcatexpert.com/blog/2010/04/01/configuring-jdbc-pool-high-concurrency
自己能力有限,翻译中难免会有失误,可能不能充分理解原作者的表达含义,自己在边学习相关知识的时候也会边修改
【转帖】置高并发jdbc连接池