项目记录——故障排查
这里将会记录一些我在企业开发中的遇到的一些长见识的Bug,以及针对别人代码的故障排查
SQL DML堵塞问题(Oracle)
Bug描述
最近项目需要接收大数据部门接口传来的数据,然后对数据做一些update操作后插入到一个表里面,后文会将该表称之为Business表。在执行这个操作的时候,意外发现执行时间过长,超过二十分钟了。.
最开始以为是数据量过大的问题,后来发现并不是,因此不得不化身为DBA寻找阻塞原因,因此记录下解决这个问题的经历:
问题排查
首先可以用以下sql分析和确定下,这个查询是不是被堵住了:
1 |
|
这SQL查询是用来检测 Oracle 数据库中的长时间堵塞(blocking)情况,它从 v$session
视图中获取有关会话的信息,特别是正在阻塞其他会话的会话和正在等待的会话的信息。
s1
和s2
是v$session
视图的别名,分别代表两个不同的会话。s1.username || '@' || s1.machine || '(SID=)' || s1.sid || ')'
和s2.username || '@' || s2.machine || '(SID=)' || s2.sid || ')'
是用来创建会话的标识信息,包括用户名、机器名、会话标识(SID),以及会话是否被堵塞的标志。s1.sql_id
,s1.sql_child_number
表示正在执行 SQL 查询的会话(s1
)的 SQL 查询 ID 和子编号。s1.sql_exec_start
是 SQL 查询的执行开始时间。s2.event
表示正在等待的会话(s2
)当前正在等待的事件。where s1.blocking_session = s2.sid
是查询的过滤条件,它会筛选出那些正在阻塞其他会话的会话(s1
)和正在等待其他会话解除堵塞的会话(s2
)。
通过上述SQL,可以找到堵塞session的SID,然后用如下sql确认阻塞原因:
1 |
|
这个SQL查询是用于查找数据库中指定会话(sid
)的详细信息,包括该会话的用户名、正在执行的SQL查询、等待事件和其他相关信息。
v$session s
和v$sql q
是v$session
和v$sql
视图的别名,分别代表会话信息和SQL查询信息。q.sql_text
是用于选择会话的SQL查询文本。s.sql_id = q.sql_id
和s.sql_child_number = q.child_number
表示会话的 SQL 查询 ID 和子编号与 SQL 查询信息的 SQL ID 和子编号匹配,以关联会话和查询。where s.sid = <blocking_session_id>
是查询的过滤条件,这里输入前面查询到的阻塞SID
通过上述的代码,我查看 s.event 发现了出问题的是update语句。
现在,我有两个选择:
(1)通过 alter system kill session '<sid><serial#>' immediate
命令直接终止堵塞会话,回滚事务;
(2)继续深挖,找到问题原因,直接修改问题代码;
显然,应该选择第二种方案,选择第一种方案再次运行还是会报错,所以继续查询v$lock
查看锁信息
1 |
|
这个SQL查询用于检查 Oracle 数据库中的锁信息,以查找哪些会话正在持有或请求锁,以及相关的会话信息。
v$lock l
和v$session s
是v$lock
视图和v$session
视图的别名,分别代表锁信息和会话信息;l.sid = s.sid
表示锁信息的会话 ID (sid
) 与会话信息的会话 ID 匹配,以关联锁和会话;l.sid
:这列表示持有或请求锁的会话的会话 ID(Session ID)。每个会话都有一个唯一的会话 ID。s.username
:这列表示持有或请求锁的会话的用户名。它标识了数据库中执行操作的用户。l.type
:这列表示锁的类型。不同的锁类型用于不同的资源或对象,例如表、行、索引等。l.id1
和l.id2
:这两列通常一起工作,表示锁的标识符。具体的含义取决于锁的类型,例如对于表锁,它们可能表示表的对象标识符。l.lmode
:这列表示锁的模式。它描述了锁的类型,例如共享锁、排他锁等。l.request
:这列表示会话是否正在请求锁,以及请求的锁类型和模式。
将查询结果与SID对应,发现阻塞的几个所分别是:
type | ID1 | ID2 | LMODE | REQUEST |
---|---|---|---|---|
AE | 100 | 0 | 4 | 0 |
TM | 168385 | 0 | 3 | 0 |
TX | 393225 | 221646 | 0 | 6 |
上述结果说明了:
第一行(AE 锁):
type
是 “AE”,表示这是一个表级别的锁。ID1
是 100,可能是表的标识符。ID2
是 0,通常表示表级别锁的 ID2 为 0。LMODE
是 4,表示持有排他锁。REQUEST
是 0,表示当前没有请求新的锁。
第二行(TM 锁):
type
是 “TM”,表示这是一个表级别的锁。ID1
是 168385,可能是表的另一个标识符。ID2
是 0,通常表示表级别锁的 ID2 为 0。LMODE
是 3,表示持有共享锁。REQUEST
是 0,表示当前没有请求新的锁。
第三行(TX 锁):
type
是 “TX”,表示这是一个事务级别的锁。ID1
是 393225,可能表示某个事务的标识符。ID2
是 221646,可能表示另一个事务的标识符。LMODE
是 0,表示没有锁(或者叫无锁)。REQUEST
是 6,表示当前正在请求新的锁。
接着,识别一下第三行的事务锁,究竟锁住的是哪段代码:
1 |
|
这个 SQL 查询用于查找特定会话(sid
)正在等待的行级锁的相关信息。以下是查询中的各个部分的含义:
do.object_name
:这表示正在等待行级锁的数据对象的名称,即表的名称或其他数据库对象的名称。row_wait_obj#
:这是会话正在等待的行级锁的对象号。它用于标识数据对象,该对象上的行级锁正被等待。do.data_object_id
:这是数据对象的唯一标识符。row_wait_file#
、row_wait_block#
、row_wait_row#
:这些列表示行级锁的具体位置信息,包括等待的文件号、块号和行号。dbms_rowid.rowid_create(1, do, data_object_id, row_wait_file#, row_wait_block#, row_wait_row#)
:这是一个函数调用,用于创建行级锁的 ROWID(行标识符),以便精确定位到正在等待的行级锁。v$session s, dba_objects do
:这部分表示查询将从v$session
视图和dba_objects
视图中检索信息。where sid = <sessionId>
:这是查询的过滤条件,用于指定要检查的特定会话的sid
。and s.row_wait_obj# = do.object_id
:这是查询的连接条件,它将会话的row_wait_obj#
与数据对象的object_id
进行匹配,以便检索正在等待的行级锁的数据对象的信息。
接着,我查询到的rowId 是 AABK/5AAFAAAAHHAAA, 把这个id放到如下的sql查询里面,就能查询出来是具体哪一行出的问题了:
1 |
|
注意Business是我的表名。
解决方案
定位发现问题行,同时是update语句出的问题之后,我发现是数据中有重名问题,同时update后面的insert语句执行过快,两个数据同时争夺同一个行锁,造成了阻塞。于是我修改了原来的java代码,对每个update语句增加了50ms的延时,解决了问题
包装对象引用问题
Bug描述
包装对象的==和equals是个老生常谈的问题,但是我最近还是踩坑了。
最近完成项目的时候遇到一个问题,我发现一个树形的下拉框里面,有的菜单有二级菜单三级菜单,有的应该有三级菜单却没有。
大概就是这样:
问题排查
于是我就去研究这个BUG是怎么产生的。
刚开始调试前端代码,发现是element-ui的el-tree组件,逻辑很简单,不应该有问题:
1 |
|
所以我把重心转向了后端代码,后端list/tree方法的serviceImpl 是一个递归查找树方法:
1 |
|
baseMapper.selectList(null)
用于拿到所有数据。
CategoryEntity 有几个核心属性: catId 是当前分类的ID,parentCid是当前分类的父分类ID,顶级分类的父分类ID是指向0的。children 是一个数组,用于存放当前分类的子分类有哪些 CategoryEntity 。
因此,在listWithTree 方法里面,item.getParentCid() == 0
用于找到所有的第一级顶级分类,然后设置其中每一个分类的children,那么,如何获取某个分类的子分类呢?
接下来就调用 getChildren 方法,这个方法需要传入当前分类和所有数据,通过 categoryEntity.getParentCid() == root.getCatId()
找到所有数据中,所有父分类指向当前分类的对象,然后继续递归查找直到查询到null为止。
刚开始的时候我研究代码逻辑并没有发现问题,断点调试的时候,如图所示的 E-book分类有自己的children,那为什么sas 分类的children 是 [] 呢?
接着我去看了下数据库,数据库也确实存在指向sas 分类的子数据。那问题来了,为什么有的分类能找到有的找不到呢?
stream流的断点调试信息稍微不足,只好通过log.info 查看每一步信息,经过我耐心的查找,我终于发现了问题所在:
**categoryEntity.getParentCid() == root.getCatId() 的判断是[]**,这就很奇怪了,这说明没有在数据库找到合适的对象,这不应该啊,因为我已经在数据库验证过数据了。
于是我想到了一个问题:getParentCid()
和 getCatId()
到底获取到了什么东西?
1 |
|
于是,我在后台代码看到了Long而不是long,就意识到了问题所在了:
对于
Long
对象,当值在 -128 到 127 之间时,Java 会使用缓存,即相同的值会共享同一个对象。而超出这个范围的值,每次都会创建新的对象。这就导致了使用==
比较时,对于超出缓存范围的值,即使它们的内容相同,也可能得到false
。
这也就解释了为什么有的能显示,有的不能显示。能显示不过是因为ID值还没有超出127而已。
解决方案
- 明白问题了,解决方案也就相对容易,只需要使用equals替换成如下代码即可:
1 |
|
真心希望不要有下次踩坑了。
EsayPoi解析Excel图片空指针
Bug描述
一个很有趣的Bug,我在这里被困扰了一天的时间。
现在有两个几乎一模一样的Excel文件,当我进行导入并解析的时候,一个Excel文件会报空指针,一个文件会正常解析并且导入。
这里的业务逻辑是:
- 业务方先下载Excel模板
- 业务方根据模板输入自己想要导入的信息,其中有一列是图片信息
- 图片信息是业务方自己粘贴进Excel的
- 最后业务方点击导入,把Excel解析,把其中的信息导入数据库
这个Bug就是业务方在这个流程中遇到的,业务方有两个几乎一模一样的Excel文件,它们的文本信息是完全一样的,唯一的区别就是图片不一样。
而且,就算把可以正常解析导入的Excel文件中的图片粘贴到出问题的Excel文件,也还是会报空指针异常,很奇怪。
问题排查
一开始我以为是图片可能覆盖了其他单元格,所以尝试拉伸了一下图片,发现没有作用。
然后仔细研究后端代码,发现这一块是调用的EasyPoi API的 *ExcelImportUtil
类的importExcelMore
*方法进行解析的。于是我继续下钻,发现更准确的说,是在调用EasyPoi API的 *PoiPublicUtil
*类的 getSheetPictures07
方法时出现的问题。
更准确的说,是在执行这个放里面如下所示的代码的时候报的错:
1 |
|
打断点,这里需要一个anchor的cell1属性和cell2属性。
然后这里的cell2属性是空,于是报了空指针。
知道错误的起源了,现在回头去找为什么cell2属性是null呢?这个属性是在哪里构造的呢?
经过漫长的寻找,最后发现是在Poi API的*XSSFDrawing
*类里面的 getAchorFromParent
方法里面构造的。
该方法如下所示:
1 |
|
不用完全理解代码,只需要看中间这里,parentXbean
将会在这里创造出anchor和它的cell2属性。
当我们使用断点调试,会发现,如果导入有问题的Excel,会走向这个判断:*parentXbean instanceof CTOneCellAnchor
*
而正常的Excel会走向这个判断:*parentXbean instanceof CTTwoCellAnchor
*
那么,问题就在CTTwoCellAnchor 和 CTOneCellAnchor上面了。
这两个类是什么呢?显然是和图片相关的东西——在这里感谢Greg Woolsey 的解释一下子点醒了我。参考:
所以,CTTwoCellAnchor是指图片的大小和位置随着单元格而变;CTOneCellAnchor是指大小固定,位置随着单元格而变
那么,这对应的就是Excel里面的图片属性:
解决方案
- 至此,问题就解决了,只需要修改Excel里面的图片属性即可。至此问题就解决了,至于更底层的原因,由于项目排期的紧张没有时间深入钻研,欢迎了解的朋友评论区留言
Json默认解析方式
Bug描述
一个很正常的操作:我在后端调用方法,向前端返回一个List的结果集。我们知道这个结果集在前端是用很多个json数据放在一个List里面的。
我将要返回的数据有这几个列:
- DESCR - 部门名
- userId - ID
- name - 姓名
- jobType - 工作类型
其中 userId,name,jobType在前端展示都是正常的,获取到了数据,但是DESCR这个列没有数据:
DESCR | userId | name | jobType |
---|---|---|---|
1 | a | A | |
2 | b | B | |
3 | c | C |
看了下后端,打断点发现数据都是正常传过来的,DESCR这个列是有值的,但是不知道为什么在前端不显示
问题排查
在前端打断点调试,发现传过来的Json数据长这个样子:
1 |
|
这就奇怪了,为啥DESCR这个列的首字母小写了呢?
后来发现,不只是DESCR这个列首字母小写了,事实上每个首字母大写的列的首字母都会被小写,在这里因为它们在这里本来就是小写的所以看不出来。
之所以会这样,探讨其背后原理是因为所有 JSON 的实现都离不开HttpMessageConverter,它是一个消息转换工具,主要实现两方面的功能:
- 将服务端返回的对象序列化成 JSON 字符串
- 将前端传来的 JSON 字符串反序列化成 Java 对象
在SpringBoot生成的依赖包中已经自动为我们导入了相关依赖。SpringMVC 自动配置了 Jackson 和 Gson 的 HttpMessageConverter。
我们公司项目默认使用的Jackson解析Json代码,jackson在序列化过程中会将大写开头的字段自动转成小写开头。
解决方案
- 给需要保留大小写的不变的属性增加*
@JsonFormat
或者@JsonProperty
*注解 - 给类加上
@JsonAutoDetect(getterVisibility=JsonAutoDetect.Visibility.NONE)
注解
BatchUpdateException: ORA-12899
Oracle SQL表设计问题
Bug描述
产品经理让我排查一下不知道什么环境下(没问是开发、测试还是正式环境)出现的一个Bug,前端上传excel更新数据失败,提示“数据库异常,请联系管理员”。
问题排查
很明显,这通常都是后端报错了,所以我按照流程复现了下该报错,然后直接看idea结果,idea报错:
Exception in thread “main” java.sql.BatchUpdateException: ORA-12899: value too large for column “xx” (actual: 208, maximum: 200)
这样的报错根据字面意思,似乎是数据超了sql某个字段的字节限制,那么通过我们在 项目记录——企业需求 中的连接迁移提到的前端代码反推后端代码的方法,找到对应的后端代码,再找到对应的实体类,看它链接的是哪个数据表,这有助于我们在公司庞大的数据库中直接定位问题表。
定位到问题表table之后,在Navicat点击“设计表”,我可以直接看到每个字段的字节限制,再对应报错的excel文件里面的内容,我可以很简单的找到对应的限制了200个字节的出问题的列。但是奇怪的是,如果一个中文编码占据2个字节,我个人感觉这个excel文件中这个列的数据的字节数并没有达到208这个标准。
查询:
1 |
|
结果是 AMERICAN_AMERICA.AL32UTF8
这说明我们Oracle数据库采用的是 AMERICAN_AMERICA.AL32UTF8
作为字符编码集,这个UTF-8编码集一个中文占据三个字节,为了验证我们的猜想,查询:
1 |
|
xx就是超过字节限制的数据内容,得到208个字节的答案,这证明我们的猜想没错。
解决方案
- 更换oracle sql中字符编码集为 *
SIMPLIFIED CHINESE_CHINA.AL32UTF8
*,这样一个中文只占据两个字节。但是显然该方法可能影响到其他表 - 要求上传excel的数据列对应内容不要太多
- 针对该字段的字节上限进行扩容