这里将会记录一些我在企业开发中的遇到的一些长见识的Bug,以及针对别人代码的故障排查

SQL DML堵塞问题(Oracle)

Bug描述

最近项目需要接收大数据部门接口传来的数据,然后对数据做一些update操作后插入到一个表里面,后文会将该表称之为Business表。在执行这个操作的时候,意外发现执行时间过长,超过二十分钟了。

最开始以为是数据量过大的问题,后来发现并不是,因此不得不化身为DBA寻找阻塞原因,因此记录下解决这个问题的经历:

问题排查

首先可以用以下sql分析和确定下,这个查询是不是被堵住了:

1
2
3
4
5
6
select s1.username || '@' || s1.machine || '(SID=)' || s1.sid || ')' as blocking_session,
s2.username || '@' || s2.machine || '(SID=)' || s2.sid || ')' as waiting_session,
s1.sql_id, s1.sql_child_number,s1.sql_exec_start,s2.event
from
v$session s1, v$session s2
where s1.blocking_session = s2.sid;

这SQL查询是用来检测 Oracle 数据库中的长时间堵塞(blocking)情况,它从 v$session 视图中获取有关会话的信息,特别是正在阻塞其他会话的会话和正在等待的会话的信息。

  • s1s2v$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
2
3
select s.sid, s.serial#, s.username, s.event, s.sql_id, s.sql_child_number, s.sql_exec_start, q.sql_text
from v$session s join v$sql q on s.sql_id = q.sql_id and s.sql_child_number = q.child_number
where s.sid = <blocking_session_id>; (这里输入前面查询到的阻塞SID)

这个SQL查询是用于查找数据库中指定会话(sid)的详细信息,包括该会话的用户名、正在执行的SQL查询、等待事件和其他相关信息。

  • v$session sv$sql qv$sessionv$sql 视图的别名,分别代表会话信息和SQL查询信息。
  • q.sql_text 是用于选择会话的SQL查询文本。
  • s.sql_id = q.sql_ids.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
2
3
4
select l.sid, s.username, l.type, l.id1, l.id2, l.lmode, l.request
from v$lock l
join
v$session s on l.sid = s.sid

这个SQL查询用于检查 Oracle 数据库中的锁信息,以查找哪些会话正在持有或请求锁,以及相关的会话信息。

  • v$lock lv$session sv$lock 视图和 v$session 视图的别名,分别代表锁信息和会话信息;
  • l.sid = s.sid 表示锁信息的会话 ID (sid) 与会话信息的会话 ID 匹配,以关联锁和会话;
  • l.sid:这列表示持有或请求锁的会话的会话 ID(Session ID)。每个会话都有一个唯一的会话 ID。
  • s.username:这列表示持有或请求锁的会话的用户名。它标识了数据库中执行操作的用户。
  • l.type:这列表示锁的类型。不同的锁类型用于不同的资源或对象,例如表、行、索引等。
  • l.id1l.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

上述结果说明了:

  1. 第一行(AE 锁):

    • type 是 “AE”,表示这是一个表级别的锁。
    • ID1 是 100,可能是表的标识符。
    • ID2 是 0,通常表示表级别锁的 ID2 为 0。
    • LMODE 是 4,表示持有排他锁。
    • REQUEST 是 0,表示当前没有请求新的锁。
  2. 第二行(TM 锁):

    • type 是 “TM”,表示这是一个表级别的锁。
    • ID1 是 168385,可能是表的另一个标识符。
    • ID2 是 0,通常表示表级别锁的 ID2 为 0。
    • LMODE 是 3,表示持有共享锁。
    • REQUEST 是 0,表示当前没有请求新的锁。
  3. 第三行(TX 锁):

    • type 是 “TX”,表示这是一个事务级别的锁。

    • ID1 是 393225,可能表示某个事务的标识符。

    • ID2 是 221646,可能表示另一个事务的标识符。

    • LMODE 是 0,表示没有锁(或者叫无锁)。

    • REQUEST 是 6,表示当前正在请求新的锁。

接着,识别一下第三行的事务锁,究竟锁住的是哪段代码:

1
2
3
4
5
6
select 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#)
from v$session s, dba_objects do
where sid = <sessionId>
and s.row_wait_obj# = do.object_id;

这个 SQL 查询用于查找特定会话(sid)正在等待的行级锁的相关信息。以下是查询中的各个部分的含义:

  1. do.object_name:这表示正在等待行级锁的数据对象的名称,即表的名称或其他数据库对象的名称。
  2. row_wait_obj#:这是会话正在等待的行级锁的对象号。它用于标识数据对象,该对象上的行级锁正被等待。
  3. do.data_object_id:这是数据对象的唯一标识符。
  4. row_wait_file#row_wait_block#row_wait_row#:这些列表示行级锁的具体位置信息,包括等待的文件号、块号和行号。
  5. dbms_rowid.rowid_create(1, do, data_object_id, row_wait_file#, row_wait_block#, row_wait_row#):这是一个函数调用,用于创建行级锁的 ROWID(行标识符),以便精确定位到正在等待的行级锁。
  6. v$session s, dba_objects do:这部分表示查询将从 v$session 视图和 dba_objects 视图中检索信息。
  7. where sid = <sessionId>:这是查询的过滤条件,用于指定要检查的特定会话的 sid
  8. and s.row_wait_obj# = do.object_id:这是查询的连接条件,它将会话的 row_wait_obj# 与数据对象的 object_id 进行匹配,以便检索正在等待的行级锁的数据对象的信息。

接着,我查询到的rowId 是 AABK/5AAFAAAAHHAAA, 把这个id放到如下的sql查询里面,就能查询出来是具体哪一行出的问题了:

1
select * from Business where rowid = 'AABK/5AAFAAAAHHAAA';

注意Business是我的表名。

解决方案

定位发现问题行,同时是update语句出的问题之后,我发现是数据中有重名问题,同时update后面的insert语句执行过快,两个数据同时争夺同一个行锁,造成了阻塞。于是我修改了原来的java代码,对每个update语句增加了50ms的延时,解决了问题


包装对象引用问题

Bug描述

包装对象的==和equals是个老生常谈的问题,但是我最近还是踩坑了。

最近完成项目的时候遇到一个问题,我发现一个树形的下拉框里面,有的菜单有二级菜单三级菜单,有的应该有三级菜单却没有。

大概就是这样:

项目记录——故障排查(2).png

问题排查

于是我就去研究这个BUG是怎么产生的。

刚开始调试前端代码,发现是element-ui的el-tree组件,逻辑很简单,不应该有问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<el-tree :data="menus"
:default-expanded-keys="expandedKeys"
:expand-on-click-node="false"
:props="defaultProps"
node-key="catId"
draggable
:allow-drop="allowDrop"
show-checkbox>

getMenus () {
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data}) => {
this.menus = data.page
})
},

所以我把重心转向了后端代码,后端list/tree方法的serviceImpl 是一个递归查找树方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public List<CategoryEntity> listWithTree() {
// 1. find all classification
List<CategoryEntity> entities = baseMapper.selectList(null);

// 2. assemble to tree (parent-son)
// 2.1 find all first-level category
List<CategoryEntity> firstLevel = entities.stream().filter((item) ->
item.getParentCid() == 0
).map((cat) -> {
cat.setChildren(getChildren(cat,entities));
return cat;
}).sorted(Comparator.comparingInt(CategoryEntity::getSort)).collect(Collectors.toList());
return firstLevel;
}

/**
* recursion get each categories' children category
* @return
*/
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all){
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(category -> {
// find children category
category.setChildren(getChildren(category,all));
return category;
}).sorted(Comparator.comparingInt(cat -> (cat.getSort() == null ? 0 : cat.getSort()))).collect(Collectors.toList());
return children;
}

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
2
3
@TableId
private Long catId;
private Long parentCid;

于是,我在后台代码看到了Long而不是long,就意识到了问题所在了:

对于 Long 对象,当值在 -128 到 127 之间时,Java 会使用缓存,即相同的值会共享同一个对象。而超出这个范围的值,每次都会创建新的对象。这就导致了使用 == 比较时,对于超出缓存范围的值,即使它们的内容相同,也可能得到 false

这也就解释了为什么有的能显示,有的不能显示。能显示不过是因为ID值还没有超出127而已。

解决方案

  • 明白问题了,解决方案也就相对容易,只需要使用equals替换成如下代码即可:
1
2
3
4
5
6
7
8
9
10
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all){
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid().equals(root.getCatId());
}).map(category -> {
// find children category
category.setChildren(getChildren(category,all));
return category;
}).sorted(Comparator.comparingInt(cat -> (cat.getSort() == null ? 0 : cat.getSort()))).collect(Collectors.toList());
return children;
}

真心希望不要有下次踩坑了。

EsayPoi解析Excel图片空指针

Bug描述

一个很有趣的Bug,我在这里被困扰了一天的时间。

现在有两个几乎一模一样的Excel文件,当我进行导入并解析的时候,一个Excel文件会报空指针,一个文件会正常解析并且导入。

这里的业务逻辑是:

  • 业务方先下载Excel模板
  • 业务方根据模板输入自己想要导入的信息,其中有一列是图片信息
  • 图片信息是业务方自己粘贴进Excel的
  • 最后业务方点击导入,把Excel解析,把其中的信息导入数据库

这个Bug就是业务方在这个流程中遇到的,业务方有两个几乎一模一样的Excel文件,它们的文本信息是完全一样的,唯一的区别就是图片不一样。

而且,就算把可以正常解析导入的Excel文件中的图片粘贴到出问题的Excel文件,也还是会报空指针异常,很奇怪。

问题排查

一开始我以为是图片可能覆盖了其他单元格,所以尝试拉伸了一下图片,发现没有作用。

然后仔细研究后端代码,发现这一块是调用的EasyPoi API的 *ExcelImportUtil类的importExcelMore*方法进行解析的。于是我继续下钻,发现更准确的说,是在调用EasyPoi API的 *PoiPublicUtil*类的 getSheetPictures07 方法时出现的问题。

更准确的说,是在执行这个放里面如下所示的代码的时候报的错:

1
2
3
...
pic.getPreferredSize()
...

打断点,这里需要一个anchor的cell1属性和cell2属性。

然后这里的cell2属性是空,于是报了空指针。

知道错误的起源了,现在回头去找为什么cell2属性是null呢?这个属性是在哪里构造的呢?

经过漫长的寻找,最后发现是在Poi API的*XSSFDrawing*类里面的 getAchorFromParent 方法里面构造的。

该方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private XSSFAnchor getAnchorFromParent(XmlObject obj) {
XSSFAnchor anchor = null;
XmlObject parentXbean = null;
XmlCursor cursor = obj.newCursor();
if (cursor.toParent()) {
parentXbean = cursor.getObject();
}
cursor.dispose();
if (parentXbean != null) {
if (parentXbean instanceof CTTwoCellAnchor) {
CTTwoCellAnchor ct = (CTTwoCellAnchor) parentXbean;
anchor = new XSSFClientAnchor(ct.getFrom(), ct.getTo());
} else if (parentXbean instanceof CTOneCellAnchor) {
CTOneCellAnchor ct = (CTOneCellAnchor) parentXbean;
anchor = new XSSFClientAnchor(getSheet(), ct.getFrom(), ct.getExt());
} else if (parentXbean instanceof CTAbsoluteAnchor) {
CTAbsoluteAnchor ct = (CTAbsoluteAnchor) parentXbean;
anchor = new XSSFClientAnchor(getSheet(), ct.getPos(), ct.getExt());
}
}
return anchor;
}

不用完全理解代码,只需要看中间这里,parentXbean 将会在这里创造出anchor和它的cell2属性。

当我们使用断点调试,会发现,如果导入有问题的Excel,会走向这个判断:*parentXbean instanceof CTOneCellAnchor*

而正常的Excel会走向这个判断:*parentXbean instanceof CTTwoCellAnchor*

那么,问题就在CTTwoCellAnchor 和 CTOneCellAnchor上面了。

这两个类是什么呢?显然是和图片相关的东西——在这里感谢Greg Woolsey 的解释一下子点醒了我。参考:

https://bz.apache.org/bugzilla/show_bug.cgi?id=61203

所以,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
2
3
{dESCR:...,userId:'1',name:'a',jobType:'A'},
{dESCR:...,userId:'2',name:'b',jobType:'B'},
...

这就奇怪了,为啥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
select userenv('language') from dual;

结果是 AMERICAN_AMERICA.AL32UTF8

这说明我们Oracle数据库采用的是 AMERICAN_AMERICA.AL32UTF8 作为字符编码集,这个UTF-8编码集一个中文占据三个字节,为了验证我们的猜想,查询:

1
select lengthb('xx') from dual;

xx就是超过字节限制的数据内容,得到208个字节的答案,这证明我们的猜想没错。

解决方案

  • 更换oracle sql中字符编码集为 *SIMPLIFIED CHINESE_CHINA.AL32UTF8*,这样一个中文只占据两个字节。但是显然该方法可能影响到其他表
  • 要求上传excel的数据列对应内容不要太多
  • 针对该字段的字节上限进行扩容