让你别乱动我MySQL的Bug,这下出生产事故了吧!

青石路 2024-04-03 15:48:07

 

一、MyBatis 替换成 MyBatis-Plus

 

 
1.背景介绍

 

一个老项目,数据库用的是 MySQL 5.7.36 , ORM 框架用的 MyBatis 3.5.0 , mysql-connector-java 版本是 5.1.26 。

 

新来了一个干练的小伙,精力充沛,看着就是一个喜欢折腾的主。

 

他就觉得 MyBatis 使用起来不够简单,要写的代码还比较多,觉得有必要替换成 MyBatis-Plus。

 

 
2.Mybatis-Plus 替换 Mybatis

 

先准备一张表 tbl_order ,然后初始化 2 条数据。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
DROP TABLE IF EXISTS `tbl_order`;CREATE TABLE `tbl_order`  (  `id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',  `order_no` varchar(50) NOT NULL COMMENT '订单号',  `pay_time` datetime(3) DEFAULT NULL COMMENT '付款时间',  `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',  `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最终修改时间',  PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB COMMENT = '订单';
INSERT INTO `tbl_order` VALUES (1, '123456', '2024-02-21 18:38:32.000', '2024-02-21 18:37:34.000', '2024-02-21 18:40:01.720');INSERT INTO `tbl_order` VALUES (2, '654321', '2024-02-21 19:33:32.000','2024-02-21 19:32:12.020', '2024-02-21 19:34:03.727');

 

图片

 

了简化演示,我就直接用 Mybatis-Plus 搭建一个示例 demo ,以此来模拟下“小伙”替换的过程。只是用 MyBatis-Plus 替换 MyBatis ,其他组件的版本暂不动。

 

Mybatis-Plus 版本就用 "小伙" 引用的版本:3.1.1 , mysql-connector-java 版本保持不变还是 5.1.26 。

 

示例代码:gitee.com/youzhibing/qsl-project/tree/master/play_it_safe

 

此时运行 com.qsl.OrderTest#orderListAllTest ,会报错,异常信息如下:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
org.springframework.dao.TransientDataAccessResourceException: Error attempting to get column 'pay_time' from result set.  Cause: java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime; Conversion not supported for type java.time.LocalDateTime; nested exception is java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime
    at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:110)    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)    at com.sun.proxy.$Proxy53.selectList(Unknown Source)    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)    at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:158)    at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:76)    at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:62)    at com.sun.proxy.$Proxy59.selectList(Unknown Source)    at com.qsl.OrderTest.orderListAllTest(OrderTest.java:28)    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)    at java.lang.reflect.Method.invoke(Method.java:498)    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)Caused by: java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1078)    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)    at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:5126)    at com.mysql.jdbc.JDBC4ResultSet.getObject(JDBC4ResultSet.java:547)    at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:5133)    at com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java)    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)    at java.lang.reflect.Method.invoke(Method.java:498)    at org.apache.ibatis.logging.jdbc.ResultSetLogger.invoke(ResultSetLogger.java:69)    at com.sun.proxy.$Proxy71.getObject(Unknown Source)    at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:38)    at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:28)    at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:81)    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:521)    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:402)    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:354)    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:328)    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:301)    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:194)    at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)    at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)    at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.doQuery(MybatisSimpleExecutor.java:67)    at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)    at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)    at java.lang.reflect.Method.invoke(Method.java:498)    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)    ... 39 more

 

注意看 Caused by:

 

图片

 

不支持的转换类型 java.time.LocalDateTime,谁不支持?mysql-connector-java 不支持!

 

那 mysql-connector-java 哪个版本支持了,答案是:5.1.37 。

 

图片

 

 
3.升级mysql-connector-java

 

将 mysql-connector-java 升级到 5.1.37 ,再执行下 com.qsl.OrderTest#orderListAllTest 。

 

图片


不再报异常,查询结果也正确。MyBatis-Plus 替换 Mybatis 似乎就完成了,顺得让人有点怀疑。

 

 
4.Conversion not supported for type java.time.LocalDateTime

 

我们再回过头去看看前面说到的异常:Conversion not supported for type java.time.LocalDateTime

 

Mybatis-Plus 替换 MyBatis 之前没这个异常,替换之后就有了这个异常,这不是 Mybatis-Plus 的问题?

 

如何找这个异常的根因了?

 

很简单,直接从异常堆栈入手。

 

图片

 

点了之后,你会发现方法很简单。

 

图片

 

这么简单的代码能有什么问题?大家注意看图中左上角 MyBatis 的版本,是 3.5.1,并不是最初的 3.5.0。

 

有小伙伴可能会问了:不是用 MyBatis-Plus 替换了 MyBatis 吗,怎么还有 Mybatis ?

 

这个问题问得真的好,我只想给你个大嘴巴子。

 

你看下 MyBatis-Plus 的官方说明:

 

图片

 

既然基于 Mybatis 3.5.0 没有抛异常,而基于 3.5.1 抛了异常, LocalDateTimeTypeHandler 在 3.5.1 肯定做了调整。

 

我们来看下调整了什么?

 

图片

 

看出什么了?

 

MyBatis 3.5.0 会处理 LocalDateTime 类型的转换(将 java.sql.Timestamp 转换成 java.time.LocalDateTime )。

 

然而,注意了,然而来了!

 

然而从  MyBatis 3.5.1 开始,不再处理 LocalDateTime (还包括:LocalDate 、 LocalTime )类型的转换。

 

而是交由 JDBC 组件,也就是 mysql-connector-java 来实现。巧的是, mysql-connector-java 5.1.26 不支持类型 LocalDateTime 。

 

那它支持哪些类型了?我们同样从异常堆栈入手。

 

图片

 

点了之后,可以看到下图。

 

图片

 

往上滑动鼠标,就可以看到支持的类型了。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {    if (type == null) {        throw SQLError.createSQLException("Type parameter can not be null",                 SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());    }        if (type.equals(String.class)) {        return (T) getString(columnIndex);    } else if (type.equals(BigDecimal.class)) {        return (T) getBigDecimal(columnIndex);    } else if (type.equals(Boolean.class) || type.equals(Boolean.TYPE)) {        return (T) Boolean.valueOf(getBoolean(columnIndex));    } else if (type.equals(Integer.class) || type.equals(Integer.TYPE)) {        return (T) Integer.valueOf(getInt(columnIndex));    } else if (type.equals(Long.class) || type.equals(Long.TYPE)) {        return (T) Long.valueOf(getLong(columnIndex));    } else if (type.equals(Float.class) || type.equals(Float.TYPE)) {        return (T) Float.valueOf(getFloat(columnIndex));    } else if (type.equals(Double.class) || type.equals(Double.TYPE)) {        return (T) Double.valueOf(getDouble(columnIndex));    } else if (type.equals(byte[].class)) {        return (T) getBytes(columnIndex);    } else if (type.equals(java.sql.Date.class)) {        return (T) getDate(columnIndex);    } else if (type.equals(Time.class)) {        return (T) getTime(columnIndex);    } else if (type.equals(Timestamp.class)) {        return (T) getTimestamp(columnIndex);    } else if (type.equals(Clob.class)) {        return (T) getClob(columnIndex);    } else if (type.equals(Blob.class)) {        return (T) getBlob(columnIndex);    } else if (type.equals(Array.class)) {        return (T) getArray(columnIndex);    } else if (type.equals(Ref.class)) {        return (T) getRef(columnIndex);    } else if (type.equals(URL.class)) {        return (T) getURL(columnIndex);//        } else if (type.equals(Struct.class)) {//                //            } //        } else if (type.equals(RowId.class)) {//            //        } else if (type.equals(NClob.class)) {//            //        } else if (type.equals(SQLXML.class)) {            } else {        if (this.connection.getAutoDeserialize()) {            try {                return (T) getObject(columnIndex);            } catch (ClassCastException cce) {                SQLException sqlEx = SQLError.createSQLException("Conversion not supported for type " + type.getName(),                         SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());                sqlEx.initCause(cce);                                throw sqlEx;            }        }                throw SQLError.createSQLException("Conversion not supported for type " + type.getName(),                 SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());    }

 

确实没有 LocalDateTime 、 LocalDate 和 LocalTime 。

 

mysql-connector-java 5.1.37 开始支持 LocalDateTime 、 LocalDate 和 LocalTime ,前面已经介绍过了,不再过多赘述。

 

总结下异常根因:MyBatis 3.5.1 开始不再处理 LocalDateTime 、 LocalDate 和 LocalTime 的转换,而 mysql-connector-java 5.1.37 之前都不支持这些类型。

 

弄清楚这个异常的来龙去脉之后,顺得是不是又理所当然一些了?

 

 
5.暴风雨的来临

 

版本上线没 2 天,该来的终究还是来了。

 

我们往表 tbl_order 中插入一条记录:INSERT INTO `tbl_order` VALUES (3, 'asdfgh', NULL, '2024-02-21 20:01:31.111', '2024-02-21 20:02:56.764'); 。

 

再执行 com.qsl.OrderTest#orderListAllTest 。

 

图片

 

此刻我就想问“小伙”:刺不刺激?

 

碰到了异常,那就找原因,同样从异常堆栈入手。

 

图片

 

看出什么了?

 

如果 getTimestamp(columnIndex) 得到的是 NULL ,不就 NullPointerException ?严谨性了?

 

修复问题要紧,我们先看哪个版本进行修复了?

 

图片

 

将 mysql-connector-java 升级到 5.1.42 。

 

图片

 

问题得以修复。

 

经此一役,“小伙”似乎成长了很多,但眼里的光却黯淡了不少。

 

 
6.mybatis-plus-issues-1114

 

无意中看到了这个issue-1114,跟我们前面分析的 Conversion not supported for type java.time.LocalDateTime 是不是同一个问题?

 

只是我们用到的数据库连接池是默认的 HikariCP 而非 Druid 。

 

结合druid/issues/3302来看,如果使用 Druid 作为数据库连接池,出现的异常可能跟我们前面分析的确实不一样。

 

所以大家需要根据自己的实际情况来分析,但针对异常的分析方法是通用的。

 

二、修了“不该修的Bug”

 

这是我亲身经历的一次事故,到现在都觉得这锅背得有点冤。

 

 
1.背景介绍

 

文件分为主文件和附属文件,主文件生成之后再生成附属文件。

 

附属文件生成的时候,会校验其依赖的主文件是否都生成了,如果有任意一个主文件未生成,依赖文件不能生成并抛出异常。

 

这个业务还是比较简单吧,但在附属文件校验的优化上,我背上了生产事故。

 

 
2.优化前的校验

 

图片

 

listFileGenerateLog 作用是根据参数查询文件生成记录,具体实现不用关注。

 

这个校验逻辑是什么?只要有任意一个主文件生成,校验就算通过了,与业务要求(主文件全部生成,才算校验通过)不匹配呀。

 

这不是妥妥的 Bug ?

 

 
3.优化后的校验

 

碰到 Bug 你能忍?我是忍不了一点,反手就是一个优化。

 

图片

 

这是不是就符合业务要求了?

 

 
4.生产异常

 

中午升级之后,稳定运行了一段时间,期间文件正常生成,没出现任何问题。

 

晚上 19 点,有个附属文件生成失败,异常提示:依赖的资源[abc_{yyyyMMdd}.txt]未生成 。

 

当时看到这个异常的第一眼,觉得既熟悉又陌生,熟悉的是这个异常信息的结构,陌生的是 abc_{yyyyMMdd}.txt ,这不是文件名吗?

 

正常来讲应该是 fileId ,是一个自增的正整数呀,怎么会是文件名了?

 

脑中瞬间闪过一个念头:数据库数据有问题?

 

一查吓一跳,这个附属文件关联主文件的字段值是:4356,abc_{yyyyMMdd}.txt ,看最终修改时间是:2021-08-21 15:22:12.652 。

 

4356 文件的文件名就是 abc_{yyyyMMdd}.txt ,正常来讲,这个关联字段的值应该是:4356 。

 

敢情这个 校验Bug 完美兼容了这个脏数据 ,所以几年了,一直没出现异常。

 

图片

 

是不是有这味了?

 

这可倒好,我把 Bug 修好,还出现问题了,你说我是不是手贱?

 

经此一役,我眼里的光又黯淡了些许。

 

总结

 

关于对组件的升级,或者对旧代码的调整,都有可能牵一发动全身,影响甚大。

 

我的观点是:能不动就不要动,改好没绩效,改出问题要背锅,吃力不讨好,又不是不能跑。

 

如果到了不得不改的地步了,那就需要全面的测试。


 
作者丨青石路
来源丨cnblogs.com/youzhibing/p/18019399
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn
 
 
 
活动推荐

 

2024 XCOPS智能运维管理人年会·广州站将于5月24日举办,深究大模型、AI Agent等新兴技术如何落地于运维领域,赋能企业智能运维水平提升,构建全面运维自治能力!码上报名,享早鸟优惠。

 

最新评论
访客 2023年08月20日

230721

访客 2023年08月16日

1、导入Mongo Monitor监控工具表结构(mongo_monitor…

访客 2023年08月04日

上面提到: 在问题描述的架构图中我们可以看到,Click…

访客 2023年07月19日

PMM不香吗?

访客 2023年06月20日

如今看都很棒

活动预告