引言:昨天晚上做了个激活服务,然后测试没问题,今天早上重新测了下,发现报异常,链接不上数据库.
先说一下发生这个Exception的大致原因:
MySQL的配置中,有一个叫做“wait_timeout"的参数,这个参数大致的意思是这样:当一个客户端连接到MySQL数据库后,如果客户端不自己断开,也不做任何操作,MySQL数据库会将这个连接保留"wait_timeout"这么长时间(单位是s,默认是28800s,也就是8小时),超过这个时间之后,MySQL数据库为了节省资源,就会在数据库端断开这个连接;当然,在此过程中,如果客户端在这个连接上有任意的操作,MySQL数据库都会重新开始计算这个时间。
这么看来,发生上面Exception的原因就是因为我的服务器和MySQL数据库的连接超过了”wait_timeout"时间,MySQL服务器端将其断开了,但是我的程序再次使用这个连接时没有做任何判断,所以就挂了。
那这个问题怎么解决呢?
查了下网上的资料,其中详细点的如下:
第一个问题:我们的服务器曾经在设计的过程中考虑过这个事情,所以服务器的主线程有一个定时的check机制,每隔半小时会发送一个"select 1"到数据库来保证连接是活动的,为什么这个check机制不起作用了呢?
第二个问题:从上面的Exception中可以得到这么一个信息:
[java] view
plaincopy
- The last packet sent successfully to the server was 43200 milliseconds ago, which is longer than the server configured value of ‘wait_timeout‘.
这个信息说的很明白,最后一个成功发到Server的包是43200毫秒之前。但是43200毫秒才43.2秒,也就是说我们的服务器43.2秒之前才和MySQL服务器通过信,怎么会发生超过”wait_timeout“的问题呢?而且MySQL数据库的配置也确实是28800秒(8小时),这又是神马情况呢?
在网上google了n长时间,倒是有不少关于这个问题的讨论,但是一直没有找到让我觉得有效的方法,只能自己结合google到结果来慢慢琢磨了。
首先,MySQL数据库那边的解决方案很单一,就是延长”wait_timeout“的数值。我看有的人直接就延长到一年了,也有人说这个值最大也就是21天,即使值设的再大,MySQL也就只识别21天(这个我没有具体去MySQL的文档中去查)。但是这是一个治标不治本的方法,即使可以一年,也还是会有断的时候,服务器可是要7x24小时在线的呀。
既然MySQL数据库那边没什么好方法,接下来就只能从程序这边来搞了。
先说说程序这边的大致结构吧:两个线程,一个线程负责查询和上面说到的check机制,另一个线程负责定时更新数据库,使用Hibernate,配置很简单,都是最基本的,没有任何关于连接池和缓存的配置,就像下面这样:
[html] view
plaincopy
- <session-factory>
- <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
- <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
- <property name="hibernate.connection.useUnicode">true</property>
- <property name="hibernate.connection.characterEncoding">UTF-8</property>
- <property name="hibernate.show_sql">true</property>
- <!-- 以下就全是mapping了,省略 -->
- </session-factory>
程序中更新的过程大致是这样:
[java] view
plaincopy
- session = org.hibernate.SessionFactory.openSession();
- transaction = session.beginTransaction();
- session.update(something);
- transaction.commit();
- session.close();
在这里,所有关于数据库Connection的连接和关闭都在Hibernate中,因此,不去挖掘Hibernate的源码是不可能了。
在挖掘Hibernate源码之前,必须明确目标:挖掘什么?
其实我的目标很明确,既然断开连接是MySQL数据库做的,那么相对于我们程序这边的问题就是我们在使用完连接之后没有调用Connection.close(),才会保留一个长连接在那里。那么,Hibernate是什么时候开启这个连接,又什么时候调用Connection.close()的呢?
接下来就是Hibernate的源码挖掘中。。。
枯燥的过程就不说了,说说挖掘出的东西:
Hibernate(忘了说了,我们用的Hibernate版本是3.3.2)在上面的那种配置之下,会有一个默认的连接池,名字叫:DriverManagerConnectionProvider;这是一个极其简单的连接池,默认会在池中保留20个连接,这些连接不是一开始Hibernate初始化时就创建好的,而是在你需要使用连接时创建出来,使用完之后才加入到池中的。这里有一个叫closeConnection(Connection conn)的方法,这个方法很NB,它直接将传入的连接不做任何处理,放到池中。而这个类内部的连接池实际是一个ArrayList,每次取得时候remove掉ArrayList的第一个连接,用完后直接用add方法加入到ArrayList的最后。
我们的程序更新时,Hibernate会通过DriverManagerConnectionProvider得到一个连接Connection,在使用完之后,调用session.close()时,Hibernate会调用DriverManagerConnectionProvider的closeConnection方法(就是上面说的那个NB方法),这个时候,该连接会直接放到DriverManagerConnectionProvider的ArrayList中,从始至终也没有地方去调用Connection的close方法。
说到这里,问题就很明显了。
第一,我们的那个”select 1“的check机制和我们服务器程序中更新的逻辑是两个线程,check机制工作时,它会向DriverManagerConnectionProvider获取一个连接,而此时更新逻辑工作时,它会向DriverManagerConnectionProvider获取另外一个连接,两个逻辑工作完之后都会将自己获得的连接放回DriverManagerConnectionProvider的池中,而且是放到那个池的末尾。这样,check机制再想check这两个连接就需要运气了,因为更新逻辑更新完之后就把连接放回池中了,而更新逻辑是定时的,check机制也是定时的,两个定时机制如果总是能错开,那么check机制check的永远都是两个中的一个连接,另外一个就麻烦了。这也就是为什么check机制不好使的原因。
第二,关于Exception信息中那个43200毫秒的问题也就能说明白了,check机制check的总是一个连接,而另外一个过期的连接被更新线程拿跑了,并且在check机制之后没多久就有更新发生,43200毫秒恐怕就是它们之间的间隔吧。
到这里问题分析清楚了,怎么解决呢?
最容易想到的方案,也是网上说的最多的方案,就是延长MySQL端”wait_timeout“的时间。我说了,治标不治本,我觉得不爽,不用。
第二个看到最多的就是用”autoReconnect = true"这个方案,郁闷的是MySQL 5之后的数据库把这个功能给去了,说会有副作用(也没具体说有啥副作用,我也懒得查),我们用的Hibernate 3.3.2这个版本也没有autoReconnect这个功能了。
第三个说的最多的就是使用c3p0池了,况且Hibernate官网的文档中也提到,默认的那个连接池非常的屎,仅供测试使用,推荐使用c3p0(让我郁闷的是我连c3p0的官网都没找到,只在sourceForge上有个项目主页)。好吧,我就决定用c3p0来搞定这个问题。
用c3p0解决这个Exception问题:(点击这里进入我参考的博客,要翻墙哦,亲!)
首先很明了,只要是池它就肯定有这个问题,除非在放入池之前就把连接关闭,那池还顶个屁用。所以我参考的博客里说到,最好的方式就是在获取连接时check一下,看看该连接是否还有效,即该Connection是否已经被MySQL数据库那边给关了,如果关了就重连一个。因此,按照这个思路,我修正了Hibernate的配置文件,问题得到了解决:
[html] view
plaincopy
- <session-factory>
- <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
- <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
- <property name="hibernate.connection.useUnicode">true</property>
- <property name="hibernate.connection.characterEncoding">UTF-8</property>
- <property name="hibernate.show_sql">true</property>
- <!-- c3p0在我们使用的Hibernate版本中自带,不用下载,直接使用 -->
- <property name="hibernate.connection.provider_class">org.hibernate.connection.C3P0ConnectionProvider</property>
- <property name="hibernate.c3p0.min_size">5</property>
- <property name="hibernate.c3p0.max_size">20</property>
- <property name="hibernate.c3p0.timeout">1800</property>
- <property name="hibernate.c3p0.max_statements">50</property>
- <!-- 下面这句很重要,后面有解释 -->
- <property name="hibernate.c3p0.testConnectionOnCheckout">true</property>
- <!-- 以下就全是mapping了,省略 -->
- </session-factory>
上面配置中最重要的就是hibernate.c3p0.testConnectionOnCheckout这个属性,它保证了我们前面说的每次取出连接时会检查该连接是否被关闭了。不过这个属性会对性能有一些损耗,引用我参考的博客上得话:程序能用是第一,之后才是它的性能(又不是不能容忍)。
当然,c3p0自带类似于select 1这样的check机制,但是就像我说的,除非你将check机制的间隔时间把握的非常好,否则,问题是没有解决的。
好了,至此,困扰我的问题解决完了。希望上面的这些整理可以为我以后碰到类似的问题留个思路,也可以为正在被此问题困扰的人提供一丝帮助。
最后补充点东西:
1,一些c3p0的属性方法总结:
datasource.c3p0.acquireIncrement=10当连接池中的连接用完时,C3P0一次性创建新连接的数目;
datasource.c3p0.minPoolSize=50连接池中保留的最小连接数。默认为15
datasource.c3p0.maxPoolSize=400连接池中保留的最大连接数。默认为15;
datasource.c3p0.initialPoolSize=50初始化时创建的连接数,应在minPoolSize与maxPoolSize之间取值。默认为3;
datasource.c3p0.maxIdleTime=1800最大空闲时间,超过空闲时间的连接将被丢弃。为0或负数则永不丢弃。默认为0;
datasource.c3p0.acquireRetryAttempts=100定义在从数据库获取新连接失败后重复尝试获取的次数,默认为30;
datasource.c3p0.acquireRetryDelay=20两次连接中间隔时间,单位毫秒,默认为1000;
datasource.c3p0.debugUnreturnedConnectionStackTraces=true
datasource.c3p0.maxStatements=0JDBC的标准参数,用以控制数据源内加载的PreparedStatement数量。但由于预缓存的Statement属 于单个Connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素,如果maxStatements与 maxStatementsPerConnection均为0,则缓存被关闭。默认为0;
datasource.c3p0.idleConnectionTestPeriod=1800隔多少秒检查所有连接池中的空闲连接,默认为0表示不检查;
datasource.c3p0.breakAfterAcquireFailure=true获取连接失败将会引起所有等待获取连接的线程抛出异常。但是数据源仍有效保留,并在下次调 用getConnection()的时候继续尝试获取连接。如果设为true,那么在尝试获取连接失败后该数据源将申明已断开并永久关闭。默认为 false;
datasource.c3p0.testConnectionOnCheckout=false因性能消耗大请只在需要的时候使用它。如果设为true那么在每个connection提交的时候都 将校验其有效性。建议使用idleConnectionTestPeriod或automaticTestTable
datasource.c3p0.autoCommitOnClose=true连接关闭时默认将所有未提交的操作回滚。默认为false;
datasource.c3p0.maxStatementsPerConnection=100连接池内单个连接所拥有的最大缓存Statement数。默认为0;
2,本问题中,再多加入<property name="hibernate.c3p0.maxIdleTime">1800</property>这一行,原因在上面属性中有.
3,记得添加两个jar包:我添加的版本是c3p0-0.9.2.1.jar和mchange-commons-java-0.2.3.4.jar
若后面有新的问题 随时补充.