修改表结构前未关闭Druid PreparedStatementCache导致异常的总结

问题现象:

本次需求需要在表中增加一个字段,DBA操作后,系统立刻出现大量报警。查看日志后,发现异常出现在数据库查询阶段:

image2018-11-8_15-31-57

在对报警信息统计后,异常有如下三种:

java.sql.SQLException: 违反协议

java.sql.SQLException: OALL8 处于不一致状态

java.sql.SQLException: Io 异常

重启应用后,异常不再抛出。

 

问题原因:

  1. 为提高访问数据库的效率,开启了druid的PreparedStatementCache(以下简称为PSCache),在修改表结构前未将其关闭。
  2. 修改表结构后,因为未关闭PSCache,其中缓存的PreparedStatement与修改后的表结构不一致,因而抛出异常,其中,java.sql.SQLException: 违反协议是SQL中包含修改了表结构的表时报出的;java.sql.SQLException: OALL8 处于不一致状态是SQL不包含修改了表结构的表时报出的。
  3. 系统中使用的druid版本较低(1.0.21),在PreparedStatement执行出错后,不能将错误的PreparedStatement释放掉,而仍将其放回池子。
  4. 另外,理论上java.sql.SQLException: 违反协议异常会导致整个Connection不可用,druid有一套释放Connection的机制,如果这套机制正常执行,无论错误的PreparedStatement是否被释放,Connection都会被释放,异常也不会一直发生。然而由于低版本druid(1.0.29前)的这套释放Connection机制不兼容系统所配置的低版本Oracle Driver(oracle.jdbc.driver.OracleDriver),Connection未能被释放,最终导致异常一直发生。

 

源码分析:

在1.0.27版本前,closePoolableStatement方法的代码如下:

image2018-11-8_16-35-48

可以看到,druid不关注PreparedStatement在执行时是否出现异常,关闭时都会将它放回PreparedStatementPool中。

在1.0.27版本后,在DruidPooledStatement中增加了一个用于记录exception数量的int,在catch住异常后调用的checkException方法中自增:

image2018-11-8_18-33-14

image2018-11-8_18-27-39

在closePoolableStatement时,若exception数量不等于0,则不将其放回PreparedStatementPool中:

image2018-11-8_18-26-10

一切看起来很美好对不对?然而并不,即使更新了1.0.27版本的druid,如果使用了旧版本的Oracle Driver(oracle.jdbc.driver.OracleDriver),这一问题仍然存在。原因在于java.sql.SQLException: 违反协议(ORA-17401)这一异常是导致Connection无法修复的Fatal异常,应当将Connection也抛弃,然而1.0.27版本的druid存在Bug,仍然将应当抛弃的这个Connection扔回了池子。

仔细阅读源码,在对PreparedStatement执行时的expection进行计数+1后,还调用了DataSource的handleCollectionException方法进行处理:

image2018-11-8_19-57-19

image2018-11-8_19-58-47

而在这个handleCollectionException方法中,会调用ExceptionSorter,判断发生的异常是不是指定错误码的、会导致Connection无法修复的Fatal异常,若是,druid会将这个Connection抛弃掉:

image2018-11-8_20-5-4

而修改表结构导致的java.sql.SQLException: 违反协议(ORA-17401)异常是包含在这一列表中的,理论上应该将这个Connection直接抛弃。然而在1.0.29版本的druid之前,druid在生成ExceptionSorter时只对新版本的Oracle Driver(oracle.jdbc.OracleDriver)指定了对应的ExceptionSorter,对于旧版本的Oracle Driver(oracle.jdbc.driver.OracleDriver)则并未指定,如果使用旧版本的Oracle Driver,就不能获取到这个判断Connection是否需要抛弃的ExceptionSorter,自然也就无法根据错误码将Connection抛弃了:

image2018-11-8_20-19-4

在1.0.29版本后,这段代码中增加了对旧版本的Oracle Driver的兼容,解决了这一问题:

image2018-11-8_20-19-4

 

解决方案:

  1. 执行修改表结构前,务必将相关系统的PSCache关闭。目前使用了PSCache却未提供开关的系统,需增加PSCache的开关。
  2. 升级druid连接池的版本至1.0.29以上,这样即使修改表结构前忘记关闭PSCache,出现异常后,出错的PreparedStatement和Connection也会被直接抛弃,不会一直报错。
  3. 将配置文件中的Oracle Driver统一为新版本的oracle.jdbc.OracleDriver