两个典型数据库异常概览
我们首先通过业务系统中发生过的两种不同异常的堆栈的概要信息,来总结一些类同和差异。
- unknown thread id异常片段
org.springframework.jdbc.UncategorizedSQLException:
### Error updating database. Cause: java.sql.SQLException: Unknown thread id: 64278282
### The error may involve com.xxx.xxx_xxxxx.xxxxxxx.dao.XxxxxxxDao.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO `t_xxxxx_xxxxx` (code, parent_code, raw_id, raw_code, raw_goal, group_id, data_type, data_source, data_support, u_key, name, system_key, remark, effect_time, expire_time, data_total, data_status, manage_status, create_user_id, create_user_name, c_t, u_t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())
### Cause: java.sql.SQLException: Unknown thread id: 64278282
; uncategorized SQLException for SQL []; SQL state [HY000]; error code [1094]; Unknown thread id: 64278282; nested exception is java.sql.SQLException: Unknown thread id: 64278282
- Lock wait timeout堆栈片段
org.springframework.dao.CannotAcquireLockException:
### Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
### The error may involve xxx.xxxxxxx.xxxxx_xxxxx.xxxxxxx.dao.XxxxxxxDao.update-Inline
### The error occurred while setting parameters
### SQL: UPDATE t_xxxxx_xxxxx SET u_t = UNIX_TIMESTAMP(), data_status = ?, data_total = ? where 1=1 AND u_key = ?
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
; SQL []; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
我们在之前的文章《记一次 【Unknown thread id: XXX】 的排查》中分析过mysql的异常由三部分组成
- 错误码(ERROR CODE)
- 状态码(SQLSTATE)
- 错误信息(ERROR MESSAGE)
在第一个UncategorizedSQLException异常堆栈的信息里这三部分均有体现。在第二个CannotAcquireLockException异常信息仅仅体现了错误信息。为什么没有体现出SQL state和error code?
SQL state和error code去了哪里?
分析这个问题,我们可以看下两个异常类的异同
- org.springframework.jdbc.UncategorizedSQLException
- org.springframework.dao.CannotAcquireLockException
1、从继承体系看类同
两个异常类虽然各自的继承链都很长,他们第一个相同的父类就是DataAccessException,而这个类继承了RuntimeException,是非受检异常。这也是spring数据访问框架对于底层异常封装的指导思想,将底层的受检异常包装为自己的非受检异常,减少上层应用的编程复杂度。
2、从架构层次来分析
dao > jdbc > 数据库
dao(数据访问)不局限于jdbc方式(还可以是odbc,也可以是redis),jdbc也不仅仅局限于mysql数据库(也可以是oracle、sqlserver)。
虽然我们讨论的是异常封装,但这种层次关系是共通的。
CannotAcquireLockException位于dao层,是更具象化的和底层实现无关的异常封装,见名知意,指的是获取锁异常。(锁并不局限于关系型数据库)
UncategorizedSQLException位于jdbc层,这一层专用于处理jdbc相关的异常封装。而UncategorizedSQLException类,见名知意,指的是未分类的sql异常。
3、从构造函数看异同
org.springframework.jdbc.UncategorizedSQLException
public UncategorizedSQLException(String task, String sql, SQLException ex) {
super(task + "; uncategorized SQLException for SQL [" + sql + "]; SQL state [" +
ex.getSQLState() + "]; error code [" + ex.getErrorCode() + "]; " + ex.getMessage(), ex);
this.sql = sql;
}
org.springframework.dao.CannotAcquireLockException
/**
* Constructor for CannotAcquireLockException.
* @param msg the detail message
*/
public CannotAcquireLockException(String msg) {
super(msg);
}
/**
* Constructor for CannotAcquireLockException.
* @param msg the detail message
* @param cause the root cause from the data access API in use
*/
public CannotAcquireLockException(String msg, Throwable cause) {
super(msg, cause);
}
两个异常类的构造函数内部都是调用了父类的构造函数,虽然各自的继承链都很长,但是最终还是会调用到RuntimeException对应的构造方法(两个重载的构造方法的第一个参数都是msg)。
- UncategorizedSQLException的msg拼了一大堆字符串,仔细看下,因为异常msg里拼接SQL state和error code信息,所以日志里能够看到。
- UncategorizedSQLException之所以能够得到SQL state和error code信息,是因为他的构造函数入参有一个SQLException类型变量ex。
小结
除了解答了这一小节的问题,通过以上的分析还是可以总结出如下知识点:
异常 | 所属框架 | 受检异常/非受检异常 | 层次 | 构造函数 |
---|---|---|---|---|
UncategorizedSQLException | spring | 非受检 | sprig框架的jdbc处理层、只封装jdbc异常 | 要求SqlException类型入参、对msg进行了加工 |
CannotAcquireLockException | spring | 非受检 | sprig框架的dao处理层、封装数据访问异常 | 不限异常类型、直接使用cause异常的msg |
CannotAcquireLockException没有指定SQLException入参并不意味着他不能封装SQLException,恰恰相反,他的封装能力是包括但不限于SQLException的,因为他的其中一个构造入参类型是Throwable,这个是所有异常的老祖宗。
CannotAcquireLockException是如何产生的呢?
1、封装了什么异常?
### Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
从以上的异常信息片段中我们可以知道CannotAcquireLockException包装的异常是com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException。
看下MySQLTransactionRollbackException的继承结构。
关于MySQLTransactionRollbackException,我们得到以下信息:
- MySQLTransactionRollbackException是mysql jdbc实现中的一个类,不属于spring。
- MySQLTransactionRollbackException是SQLException的子类
- MySQLTransactionRollbackException是受检异常(需要try-catch的),因为SQLException继承的是Exception(没有继承RuntimeException的异常都是受检异常)。
MySQLTransactionRollbackException既然是SQLException的子类,那么他就是一个范围更具体的异常类型的描述,见名猜意,应该指的就是事务回滚异常。同时因为他是一个受检异常,按照spring的套路,他应该被封装为一个非受检异常,在我们的异常信息里可以看到他被封装在CannotAcquireLockException中了。
到这一步只能说明dao层的CannotAcquireLockException是可以封装jdbc层的MySQLTransactionRollbackException的,那么CannotAcquireLockException是否还可以封装其他异常呢?又是在什么时候封装的呢?
2、在何处被调用(创建)
鉴于我们的应用的数据库是mysql,采用jdbc方式连接,在应用里搜索CannotAcquireLockException的调用方的时候,其实只有一处。那就是org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator的doTranslate方法。
protected DataAccessException doTranslate(String task, String sql, SQLException ex) {
SQLException sqlEx = ex;
// 省略了部分代码
// 省略了部分代码(如果有自定义的转换逻辑,则走自定义的转换逻辑,并返回)
// 省略了部分代码(如果有自定义的转换器,则先尝试用自定义的转换器转换,转换成功了就返回)
// Check SQLErrorCodes with corresponding error code, if available.
if (this.sqlErrorCodes != null) {
String errorCode;
// 这个地方是否根据sql state值来转换异常。默认是false的。
if (this.sqlErrorCodes.isUseSqlStateForTranslation()) {
errorCode = sqlEx.getSQLState();
}
else {// 默认会走到这里,也就是说根据sql异常的error code来转换异常
// Try to find SQLException with actual error code, looping through the causes.
// E.g. applicable to java.sql.DataTruncation as of JDK 1.6.
SQLException current = sqlEx;
while (current.getErrorCode() == 0 && current.getCause() instanceof SQLException) {
current = (SQLException) current.getCause();
}
errorCode = Integer.toString(current.getErrorCode());
}
if (errorCode != null) {
// Look for defined custom translations first.
CustomSQLErrorCodesTranslation[] customTranslations = this.sqlErrorCodes.getCustomTranslations();
// 省略了部分代码(如果有自定义的转换规则【错误码->异常类】,转换成功了就返回)
// Next, look for grouped error codes.
if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
logTranslation(task, sql, sqlEx, false);
return new BadSqlGrammarException(task, sql, sqlEx);
}
// 省略了很多 else if 片段,我们重点关注的地方在这里
else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) {// 这个判断的意思就是把当前的错误码拿到cannotAcquireLockCodes这个错误码集合里二分法搜索下,如果找到了,就意味着这个异常可以归类为CannotAcquireLockException。
logTranslation(task, sql, sqlEx, false);
return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);
}
// 省略了很多 else if 片段
}
}
// 省略了部分代码
return null;
}
经过以上代码片段的分析,我们可以得知,spring的很多针对dao的自定义异常实际上也是根据sql错误码或者sqlstate值来归类的。每个自定义异常都会对应一个或者多个错误码或者sqlstate值集合。只要被转换的SQLException的错误码或者sqlstate值在某个集合内,那么就会被包装成对应的异常。
在我们例子中,被包装的异常是MySQLTransactionRollbackException,他是SQLException的子类,那么他的errorCode是多少呢?
3、errorCode哪里来?
public class MySQLTransactionRollbackException extends MySQLTransientException implements DeadlockTimeoutRollbackMarker {
static final long serialVersionUID = 6034999468737801730L;
public MySQLTransactionRollbackException(String reason, String SQLState, int vendorCode) {
super(reason, SQLState, vendorCode);
}
public MySQLTransactionRollbackException(String reason, String SQLState) {
super(reason, SQLState);
}
public MySQLTransactionRollbackException(String reason) {
super(reason);
}
public MySQLTransactionRollbackException() {
super();
}
}
通过MySQLTransactionRollbackException的代码我们可以看到,他并没有一个属性定义了自己的专属错误码,他的错误码和sql state也都是构造时指定的。这也就是说MySQLTransactionRollbackException也不是仅表示一个sql错误,也是代指一类sql 错误。
所以我们还需要继续看MySQLTransactionRollbackException都是在什么时候实例化的。通过查找这个类的引用我们会发现,MySQLTransactionRollbackException是在com.mysql.jdbc.SQLError类的createSQLException方法中实例化的,代码片段如下:
public static SQLException createSQLException(String message, String sqlState, int vendorErrorCode, boolean isTransient, ExceptionInterceptor interceptor,
Connection conn) {
try {
SQLException sqlEx = null;
if (sqlState != null) {
if (sqlState.startsWith("08")) {
// 省略了很多代码,实例化 MySQLTransientConnectionException
} else if (sqlState.startsWith("22")) {
// 省略了很多代码,实例化 MySQLDataException
} else if (sqlState.startsWith("23")) {
// 省略了很多代码,实例化 MySQLIntegrityConstraintViolationException
} else if (sqlState.startsWith("42")) {
// 省略了很多代码,实例化 MySQLSyntaxErrorException
} else if (sqlState.startsWith("40")) {
if (!Util.isJdbc4()) {
sqlEx = new MySQLTransactionRollbackException(message, sqlState, vendorErrorCode);
} else {
sqlEx = (SQLException) Util.getInstance("com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException", new Class[] {
String.class, String.class, Integer.TYPE }, new Object[] { message, sqlState, Integer.valueOf(vendorErrorCode) }, interceptor);
}
} else if (sqlState.startsWith("70100")) {
// 省略了很多代码
} else {
sqlEx = new SQLException(message, sqlState, vendorErrorCode);
}
} else {
sqlEx = new SQLException(message, sqlState, vendorErrorCode);
}
return runThroughExceptionInterceptor(interceptor, sqlEx, conn);
} catch (SQLException sqlEx) {
SQLException unexpectedEx = new SQLException("Unable to create correct SQLException class instance, error class/codes may be incorrect. Reason: "
+ Util.stackTraceToString(sqlEx), SQL_STATE_GENERAL_ERROR);
return runThroughExceptionInterceptor(interceptor, unexpectedEx, conn);
}
}
通过以上代码我们注意到
sqlState.startsWith("40")
意味着如果sql State是以40开头的sql错误,那么都会归类为MySQLTransactionRollbackException。如果是这样,我们是不是就可以反向推出来
【Lock wait timeout exceeded; try restarting transaction】这个sql 错误信息所对应的sql state 值是以40开头的?
SQLSTATE疑云
于是我们可以再次打开mysql官网关于服务端错误的描述。从中我们搜索这一句错误消息。会得到如下信息。
Error number: 1205; Symbol: ER_LOCK_WAIT_TIMEOUT; SQLSTATE: HY000
Message: Lock wait timeout exceeded; try restarting transaction
从mysql官网里我们可以看到,【Lock wait timeout exceeded; try restarting transaction】sql错误对应的错误码是【1205】,对应的错误标识是【ER_LOCK_WAIT_TIMEOUT】,对应的sqlstate是【HY000】。这个HY000我们之前分析过,当mysql自己定义的错误信息在sqlstate规范中找不到一个对应的代码时就用【HY000】代替。如果是这样,那么就不会满足sqlState.startsWith(“40”),也就不会产生MySQLTransactionRollbackException,并且也不会满足其他的比对,只会最终路由到else里产生一个SQLException而已。可事实是我们确实得到了MySQLTransactionRollbackException。为什么?要解开这个疑问还得继续分析是谁调用了SQLError的createSQLException方法。
继续往上追述调用方就会追溯到MysqlIO类的checkErrorPacket方法。这个方法是检测并处理mysql错误的。看代码之前有必要先了解一下mysql错误包的结构。
private void checkErrorPacket(Buffer resultPacket) throws SQLException {
int statusCode = resultPacket.readByte();
// mysql协议规定,服务端返回的数据包的第一个字节如果是0xff,则意味着这是一个错误包
if (statusCode == (byte) 0xff) {
String serverErrorMessage; // 用来存储服务端错误信息
int errno = 2000; // 定一个错误编号
if (this.protocolVersion > 9) {
// 从数据包里读取错误码。协议里规定,错误码从第二字节开始
errno = resultPacket.readInt();
String xOpen = null;
// 从包里解析错误信息
serverErrorMessage = resultPacket.readString(this.connection.getErrorMessageEncoding(), getExceptionInterceptor());
// 如果错误信息第一个字符是 #,说明这个协议的结构是:1个字节的标识 + 5个字节的sqlstate + 其余剩下的都是错误描述信息
if (serverErrorMessage.charAt(0) == '#') {
// 校验一下长度
if (serverErrorMessage.length() > 6) {
// 这个截取正好截取出5个字节的内容,也正好就是数据包里的sqlstate。
xOpen = serverErrorMessage.substring(1, 6);
// 再截取剩下就都是错误描述信息。
serverErrorMessage = serverErrorMessage.substring(6);
// 重点在这里,如果发现这个xOpen【sqlstate】是【HY000】
if (xOpen.equals("HY000")) {
// 那么再根据错误码去转换一个更具体的sqlstate,这个转换操作是在SQLError里做的。
xOpen = SQLError.mysqlToSqlState(errno, this.connection.getUseSqlStateCodes());
}
} else {
xOpen = SQLError.mysqlToSqlState(errno, this.connection.getUseSqlStateCodes());
}
} else {
xOpen = SQLError.mysqlToSqlState(errno, this.connection.getUseSqlStateCodes());
}
// 省略了部分代码
// 通过SQLError构造异常,然后抛出
throw SQLError.createSQLException(errorBuf.toString(), xOpen, errno, false, getExceptionInterceptor(), this.connection);
}
// 省略了部分代码
}
}
我们从SQLError.createSQLException追述sqlstate的由来,最终又路由回SQLError的mysqlToSqlState方法。
static String mysqlToSqlState(int errno, boolean useSql92States) {
// 是否使用SQL标准状态码取代“传统的”X/Open/SQL状态码。默认为true
if (useSql92States) {
return mysqlToSql99(errno);
}
return mysqlToXOpen(errno);
}
private static String mysqlToSql99(int errno) {
// 把基本类型转为包装类型,显示用valueOf,是可以有机会使用到整型常量池,在-128到+127范围内的,可以直接返回,不需要new Integer(errno)
// 如果没有这一步,直接使用errno也没问题,但会触发自动装箱机制,导致每次都是new Integer(errno)
Integer err = Integer.valueOf(errno);
// 重点就是从这个map结构里去检查下有没有配置errno对应的sqlstate值,有就返回。
if (mysqlToSql99State.containsKey(err)) {
return mysqlToSql99State.get(err);
}
return SQL_STATE_CLI_SPECIFIC_CONDITION;
}
我们最终会在代码里找到初始化mysqlToSql99State映射关系的代码
mysqlToSql99State.put(MysqlErrorNumbers.ER_LOCK_WAIT_TIMEOUT, SQL_STATE_ROLLBACK_SERIALIZATION_FAILURE);
MysqlErrorNumbers.ER_LOCK_WAIT_TIMEOUT是个常量,定义在MysqlErrorNumbers类中。
public final static int ER_LOCK_WAIT_TIMEOUT = 1205; //SQLSTATE: HY000 Message: Lock wait timeout exceeded; try restarting transaction
SQL_STATE_ROLLBACK_SERIALIZATION_FAILURE也是个常量,定义在SQLError类中。
public static final String SQL_STATE_ROLLBACK_SERIALIZATION_FAILURE = "40001";
到此我们就查到了mysql服务器上产生的sqlstate在流转到在jdbc层是可能被转换的。转换的依据是根据错误码来定的。
串联一下整个流程
- 数据库层:mysql发生了【1205、HY000、Lock wait timeout exceeded; try restarting transaction】异常。
- jdbc层:数据包流转到jdbc层的MysqlIO时发现sqlstate是【HY000】,于是根据错误码【1205】把sqlstate转为【40001】继续往下游流转。
- jdbc层:在jdbc层的【SQLError】类中构造SqlException时,根据【40001】构造除了一个对应的子类MySQLTransactionRollbackException。
- spring层:而这个MySQLTransactionRollbackException继续向外抛出,在spring层针对jdbc的专有处理逻辑中SQLErrorCodeSQLExceptionTranslator中,根据MySQLTransactionRollbackException携带的错误码【注意是错误码,不是sqlstate。这一步是根据useSqlStateForTranslation标识来决定的,默认false】又将其包装成了CannotAcquireLockException。
SQLErrorCodeSQLExceptionTranslator中之所以会构造出CannotAcquireLockException是因为下面代码执行为ture。也就是说错误码【1205】在getCannotAcquireLockCodes()集合中。
Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0
那么getCannotAcquireLockCodes()的集合里都有什么,在哪里定义的?
getCannotAcquireLockCodes()解析
我们在SQLErrorCodes类中摘取了以下代码。
private String[] cannotAcquireLockCodes = new String[0];
public String[] getCannotAcquireLockCodes() {
return this.cannotAcquireLockCodes;
}
SQLErrorCodes类的cannotAcquireLockCodes属性里的值肯定包含1205。那这个集合是在哪里赋值的呢。答案是:org.springframework.jdbc.support包下的sql-error-codes.xml。摘取了部分定义如下(里面还有其他数据库的定义,ms-sql、h2等)。
<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>
这就是标准的spring的bean定义信息,一个类定义了多个bean,每个数据库对应一个bean,每个bean的属性值都是和自己数据库实现相关的code值。MySQL bean的cannotAcquireLockCodes属性只包含一个值就是1205.
总结
下图是mysql异常在框架层面的流转过程,从下向上看。