问题

有业务反馈在修改一个表字段长度后,Java应用不停的报下面的错误,但是越往后错误越少,过了15分钟错误就没有再发生。

### Error querying database.  Cause: org.postgresql.util.PSQLException: ERROR: cached plan must not change result type 

原因

调查判断原因是修改字段长度导致执行计划缓存失效,继续使用之前的预编译语句执行会失败。

很多人遇到过类似错误,比如:

但是,有两个疑问没有解释清楚。

  1. 以前业务也改过字段长度,但为什么没有触发这个错误?
  2. 这个错误能否自愈?

下面是进一步的分析

PostgreSQL中抛出此异常的代码如下:

						
  1. static List *
  2. RevalidateCachedQuery(CachedPlanSource *plansource,
  3.                       QueryEnvironment *queryEnv)
  4. {
  5.         if (plansource->fixed_result)
  6.             ereport(ERROR,
  7.                     (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
  8.                      errmsg("cached plan must not change result type")));
  9. ...
  10. }

pgjdbc代码里有对该异常的判断,发生异常后,后续的执行会重新预编译,不会继续使用已经失效的预编译语句。这说明pgjdbc对这个错误有容错或自愈能力。

							
  1. protected boolean willHealViaReparse(SQLException e) {
  2. ...
  3.     // "cached plan must not change result type"
  4.     String routine = pe.getServerErrorMessage().getRoutine();
  5.     return "RevalidateCachedQuery".equals(routine) // 9.2+
  6.         || "RevalidateCachedPlan".equals(routine); // <= 9.1
  7.   }

发生条件

经验证,使用Java应用时本故障的发生条件如下:

  1. 使用非自动提交模式
  2. 使用prepareStatement执行相同SQL 5次以上
  3. 修改表字段长度
  4. 表字段长度修改后第一次使用prepareStatement执行相同SQL

测试验证

以下代码模拟Java连接多次出池->执行->入池,中途修改字段长度。可以复现本问题

												
  1. Connection conn = DriverManager.getConnection(...);
  2.      conn.setAutoCommit(false); //自动提交模式下,不会出错,pgjdbc内部会处理掉
  3.      String sql = "select c1 from tb1 where id=1";
  4.      PreparedStatement prest =conn.prepareStatement(sql);
  5.     
  6.      for(int i=0;i<5;i++)
  7.      {
  8.           System.out.println("i: " + i);
  9.           prest =conn.prepareStatement(sql);
  10.           ResultSet rs = prest.executeQuery();
  11.           prest.close();
  12.           conn.commit();
  13.      }
  14.     
  15.      //在这里设置断点,手动修改字段长度: alter table tb1 alter c1 type varchar(118);
  16.     
  17.      for(int i=5;i<10;i++)
  18.      {
  19.           System.out.println("i: " + i);
  20.           try {
  21.           prest =conn.prepareStatement(sql);
  22.           ResultSet rs = prest.executeQuery();
  23.           prest.close();
  24.           conn.commit();
  25.           } catch (SQLException e) {
  26.                  System.out.println(e.getMessage());
  27.                  conn.rollback();
  28.              }
  29.      }
  30.      conn.close();

测试程序执行结果如下:

											
  1. i: 0
  2. i: 1
  3. i: 2
  4. i: 3
  5. i: 4
  6. i: 5
  7. ERROR: cached plan must not change result type
  8. i: 6
  9. i: 7
  10. i: 8
  11. i: 9

回避

  1. 在不影响业务逻辑的前提下,尽量使用自动提交模式
  2. 修改表字段长度后重启应用,或者在业务发生该SQL错误后重试(等每个Jboss缓存的连接都抛出一次错误后会自动恢复)
10-16 20:37