问题
最近公司想把原Oracle数据库都迁移到Mysql,这个切换需要一段时间过渡,所以存在Oracle、Mysql在项目中同时使用的情况。这样就需要使用多数据源的技术。多数据源配置本身比较简单,但有一个场景出现了问题。考虑如下代码:
// 通过try-catch实现insertOrUpdate Data data = new Data(); try{ dataMapper.insert(data); } catch (DuplicateKeyException e) { dataMapper.update(data); }
可是意外发生了,这里DuplicateKeyException异常并没有被捕获,或者说这里抛出的异常并不是我们想要捕获的,而是抛出的DataAccessResourceFailureException异常,异常栈信息片段如下。
org.springframework.dao.DataAccessResourceFailureException: ### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (...) violated ### The error may involve DataMapper.insert-Inline ### The error occurred while setting parameters ### SQL: INSERT INTO ... VALUES (?, ?, ?, ?, ?) ### Cause: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (PBOC.PK_PB_NDES_DATA_RELATION) violated ; SQL []; ORA-00001: unique constraint (...) violated ; nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (...) violated at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:251) ~[spring-jdbc-4.2.0.RELEASE.jar:4.2.0.RELEASE] at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73) ~[spring-jdbc-4.2.0.RELEASE.jar:4.2.0.RELEASE] at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73) ~[mybatis-spring-1.2.2.jar:1.2.2] at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:371) ~[mybatis-spring-1.2.2.jar:1.2.2] at com.sun.proxy.$Proxy29.insert(Unknown Source) ~[na:na] at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:240) ~[mybatis-spring-1.2.2.jar:1.2.2]
排查问题
打开Spring源码 DataAccessResourceFailureException这个类,看了一眼注释,发现跟预期完全不对路啊。上面的异常栈已经说了,driver层给出的异常 java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (…) violated 说明这个异常确实是唯一键冲突,但到spring这里异常类型出问题了。
/** * Data access exception thrown when a resource fails completely: * for example, if we can't connect to a database using JDBC. * * @author Rod Johnson * @author Thomas Risberg */ @SuppressWarnings("serial") public class DataAccessResourceFailureException extends NonTransientDataAccessResourceException {
没办法,只能去看spring在异常转换的逻辑了,先根据异常栈定位到SQLErrorCodeSQLExceptionTranslator,很快就找到了如下代码片段。
else if (Arrays.binarySearch(this.sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) { logTranslation(task, sql, sqlEx, false); return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx); } else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) { logTranslation(task, sql, sqlEx, false); return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx); }
发现spring抛出的异常类型是根据sqlErrorCodes来判断的,那么下一步就得看sqlErrorCodes是如何被定义的。通过跟踪代码,找到了sql-error-codes.xml,相关代码如下:
<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="badSqlGrammarCodes"> <value>1054,1064,1146</value> </property> <property name="duplicateKeyCodes"> <value>1062</value> </property> <property name="dataIntegrityViolationCodes"> <value>630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557</value> </property> <property name="dataAccessResourceFailureCodes"> <value>1</value> </property> <property name="cannotAcquireLockCodes"> <value>1205</value> </property> <property name="deadlockLoserCodes"> <value>1213</value> </property> </bean> <bean id="Oracle" class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="badSqlGrammarCodes"> <value>900,903,904,917,936,942,17006,6550</value> </property> <property name="invalidResultSetAccessCodes"> <value>17003</value> </property> <property name="duplicateKeyCodes"> <value>1</value> </property> <property name="dataIntegrityViolationCodes"> <value>1400,1722,2291,2292</value> </property> <property name="dataAccessResourceFailureCodes"> <value>17002,17447</value> </property> <property name="cannotAcquireLockCodes"> <value>54,30006</value> </property> <property name="cannotSerializeTransactionCodes"> <value>8177</value> </property> <property name="deadlockLoserCodes"> <value>60</value> </property> </bean>
DataAccessResourceFailureException对应的code是dataAccessResourceFailureCodes,我们可以通过下表猜出一些端倪了。我在Oracle中执行sql,返回错误码ORA-00001,却被当成Mysql的错误码进行了转化,从而得到DataAccessResourceFailureException。
Mysql对应的code 1 是dataAccessResourceFailureCodes
Oracle对应的code 1 是duplicateKeyCodes
问题确认
虽然有一些端倪,但要我们的目标是确认bug并修复问题。那继续看代码,下一个问题在于SQLErrorCodeSQLExceptionTranslator中的sqlErrorCodes变量是如何被初始化的,这里把SQLErrorCodeSQLExceptionTranslator做了一些精简如下:
/** Error codes used by this translator */ private SQLErrorCodes sqlErrorCodes; public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) { this(); setDataSource(dataSource); } public void setDataSource(DataSource dataSource) { this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dataSource); }
这里即可以想到逻辑错误在哪了,sqlErrorCodes是根据dataSource得到的,而我们的dataSource是DynamicDataSource,是无法直接知道DbType的,那么还是继续往里看SQLErrorCodesFactory.getInstance().getErrorCodes是怎么做的,直接上代码:
public SQLErrorCodes getErrorCodes(DataSource dataSource) { Assert.notNull(dataSource, "DataSource must not be null"); if (logger.isDebugEnabled()) { logger.debug("Looking up default SQLErrorCodes for DataSource [" + dataSource + "]"); } synchronized (this.dataSourceCache) { // Let's avoid looking up database product info if we can. SQLErrorCodes sec = this.dataSourceCache.get(dataSource); if (sec != null) { if (logger.isDebugEnabled()) { logger.debug("SQLErrorCodes found in cache for DataSource [" + dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()) + "]"); } return sec; } // We could not find it - got to look it up. try { String dbName = (String) JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName"); if (dbName != null) { if (logger.isDebugEnabled()) { logger.debug("Database product name cached for DataSource [" + dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()) + "]: name is '" + dbName + "'"); } sec = getErrorCodes(dbName); this.dataSourceCache.put(dataSource, sec); return sec; } } catch (MetaDataAccessException ex) { logger.warn("Error while extracting database product name - falling back to empty error codes", ex); } } // Fallback is to return an empty SQLErrorCodes instance. return new SQLErrorCodes(); }
这里有两个关键点,JdbcUtils.extractDatabaseMetaData和dataSourceCache。先看dataSourceCache,这里直接根据<dataSource, SQLErrorCodes>做了一层缓存,那么问题已然实锤。我每次使用的SQLErrorCodes是需要根据dataSource可取到的Connection信息来的,加一层缓存是肯定不行的。至此问题原因已经确诊,下一步是考虑该如何修复。
修复
直接修改Spring源码肯定是行不通的,考虑SqlSessionTemplate是Spring提供给我们操作数据库的工具,那么考虑在SqlSessionTemplate上做文章。发现如下构造函数,我们使用这个构造函数,传入我们自定义的PersistenceExceptionTranslator不就好了吗。
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator)
相关代码如下:
spring_dataSource.xml
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dynamicDataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean id="myBatisExceptionTranslator" class="xxx.multidatasource.plugin.MyBatisExceptionTranslator"> <constructor-arg ref="sqlSessionFactory"/> </bean> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> <constructor-arg index="1" value="SIMPLE" /> <constructor-arg index="2" ref="myBatisExceptionTranslator" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="..." /> <property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate"/> </bean>
MyBatisExceptionTranslator.java
// 这个类直接复制了Mybatis的实现 public class MyBatisExceptionTranslator implements PersistenceExceptionTranslator { private final DataSource dataSource; private SQLExceptionTranslator exceptionTranslator; public MyBatisExceptionTranslator(SqlSessionFactory sqlSessionFactory) { this.dataSource = sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(); this.initExceptionTranslator(); } /** * {@inheritDoc} */ public DataAccessException translateExceptionIfPossible(RuntimeException e) { if (e instanceof PersistenceException) { // Batch exceptions come inside another PersistenceException // recursion has a risk of infinite loop so better make another if if (e.getCause() instanceof PersistenceException) { e = (PersistenceException) e.getCause(); } if (e.getCause() instanceof SQLException) { this.initExceptionTranslator(); return this.exceptionTranslator.translate(e.getMessage() + "\n", null, (SQLException) e.getCause()); } return new MyBatisSystemException(e); } return null; } /** * Initializes the internal translator reference. */ private synchronized void initExceptionTranslator() { if (this.exceptionTranslator == null) { // 这里改成使用自定义的DacSQLErrorCodeSQLExceptionTranslator this.exceptionTranslator = new DacSQLErrorCodeSQLExceptionTranslator(this.dataSource); } } }
DacSQLErrorCodeSQLExceptionTranslator.java
// 改造思路很简单,每次translate时都要确定一次sqlErrorCodes,再走原来的SQLErrorCodeSQLExceptionTranslator逻辑即可 public class DacSQLErrorCodeSQLExceptionTranslator implements SQLExceptionTranslator { protected final Log logger = LogFactory.getLog(this.getClass()); private DataSource dataSource; public DacSQLErrorCodeSQLExceptionTranslator(DataSource dataSource) { this.dataSource = dataSource; } @Override public DataAccessException translate(String task, String sql, SQLException ex) { String dbName = null; try { dbName = (String) JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName"); if (dbName != null) { if (logger.isDebugEnabled()) { logger.debug("Database product name cached for DataSource [" + dataSource.getClass().getName() + '@' + Integer.toHexString(dataSource.hashCode()) + "]: name is '" + dbName + "'"); } } } catch (MetaDataAccessException mdaEx) { logger.warn("Error while extracting database product name - falling back to empty error codes", mdaEx); } SQLErrorCodes sqlErrorCodes = (dbName == null) ? new SQLErrorCodes() : SQLErrorCodesFactory.getInstance().getErrorCodes(dbName); return new SQLErrorCodeSQLExceptionTranslator(sqlErrorCodes).translate(task, sql, ex); } }