mysql异常在spring、mybatis里如何流转的

两个典型数据库异常概览

我们首先通过业务系统中发生过的两种不同异常的堆栈的概要信息,来总结一些类同和差异。

  • 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。
小结

除了解答了这一小节的问题,通过以上的分析还是可以总结出如下知识点:

异常所属框架受检异常/非受检异常层次构造函数
UncategorizedSQLExceptionspring非受检sprig框架的jdbc处理层、只封装jdbc异常要求SqlException类型入参、对msg进行了加工
CannotAcquireLockExceptionspring非受检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异常在框架层面的流转过程,从下向上看。
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值