IDEA调试模式下,单步执行某修改方法后,数据库内容没有更新,同时也无法手动修改对应数据
IDEA、调试模式、单步执行、springboot、mybatis、mapper、MySQL、事务、锁
本想记下完整的技术栈原因,奈何涉及的东西有点多,管不了那么多了,只能先把现象先记录下来,假定一下原因,深层原因只能等有机会的时候再写了。
一定要戒除完美主义,对就算出事故但影响范围也不会很大不很重要的东西,万不可追求0失误,成本太高。
背景
- 技术框架:springboot、mybatis、MySQL5.6、idea2023
- 业务场景:不好细说,对业务系统中的一条信息做一项操作,操作的结果是改变这条信息的若干状态。此次调试的目的是观察代码运行时数据库的改变(大概吧,有点久远记不清了)。
问题描述
以下过程依次执行,但并不是一次调试过程,不是一次性发现了这些现象
- 记录有问题的请求数据,用idea打开表,筛选到目标数据。
- IDEA调试模式运行,在某行修改数据的代码打了一个断点,发请求进入断点。
- 鼠标滑动选中 改数据代码,
alt
+f8
打开表达式计算器。 - 找到刚才筛选的数据,手动修改一下,发现修改成功。
- 在计算器中运行一下修改数据的代码。
- 刷新筛选的数据,发现数据没有变化,但是尝试手动修改数据时,发现修改内容无法提交到数据库。
- 点击debug窗口的绿色三角继续运行按钮,或按
f9
,跳过本次调试。 - 再次刷新筛选的数据,发现数据更新了。
- 手动修改,发现可以修改成功。
分析
估计是MySQL锁在这捣鬼,查来查去东西有点深有点多,先简单记录一下。
反复执行调试过程,不难发现是在执行修改代码后,才会出现各种稀奇的现象。
猜测是为保证事务完整性,修改数据后给数据库加了锁。
而这个锁,由于MySQL的事务隔离级别默认是可重复读
,所以用的是行级锁。
调试时尝试修改别行数据,发现确实可以修改。
但是,如果这里有锁的话,这个锁是加在索引上的,不是在数据上。然而昨天学习了下MVCC,推测这也可能不是锁机制,而是可重复读的快照读搞的鬼。
快照读有一个限制条件,那就是在一个事务中。idea这里数次查询表究竟是不是一个事务不好搜集资料,但可以实验证明锁的存在性:
调试时,在程序修改数据之前,手动筛选数据,然后单步执行让程序修改数据,然后再次筛选查询发现数据无变化。
如果这手动操作都是在一个事务中,第二次查询用的就是快照读,然后,我手动更新一条别行的数据,就能触发当前读,我再一次筛选应该就会看到刚刚程序修改的结果。
如果手动更新后看不到程序修改的结果,这不能说明手动查询不在一个事务,但能说明程序对数据的修改未完全提交到数据库,因而可能有锁的存在。
如果手动更新能看到程序结果,才能说明手动查询是在一个事务中,同时证明此处无锁。
实验结果:
首次实验(失败),
过程不规范,发出请求进入断点后,手动修改了一下数据(因为忘了先把数据改成非目标数据),
然后打开计算器执行代码,手动刷新,数据无变化,单步执行代码,数据无变化,查看日志,发现修改行数为0,f9放行代码,发现响应为失败。手动查询,数据无变化。
再次实验(失败),
手动刷新数据,重发请求进入断点,单步执行,发现数据无变化,放行代码,数据无变化。屏蔽断点,多次请求,数据无变化。检查数据,数据无问题。
这似乎说明手动窗口和一次请求处理都是一个事务?关闭手动查表窗口,重新打开,筛选、搜索,查看数据是上次的数据。 突然发现程序运行和手动查询用的不是一个数据库,至此所有实验破产。
重新请求进入断点(失败),发现数据还是没有变化,没有结果。
发送请求进入断点,手动筛选、修改数据成功,计算器执行修改语句成功1行,手动查询 数据无变化,手动修改目标行对应列数据 提交缓慢、失败,idea提示:
java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953) at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1092) at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1040) at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1348) at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1025) in RemotePreparedStatementImpl.executeUpdate(RemotePreparedStatementImpl.java:293)
刷新数据放弃修改,目标行数据无变化,
手动修改相邻行对应列数据,无法提交,提示同上,刷新,
手动修改目标行无关列数据,无法提交,提示同上,刷新,
手动修改相邻行无关列数据,无法提交,提示同上,刷新,
手动修改任意行无关列数据,提交成功,重新筛选,目标行数据无变化,无法修改同一筛选条件下的相邻行无关列(刚刚修改成功的列)。
单步执行修改数据代码,成功1行,手动查询数据无变化,
放行代码,手动查询出现目标数据,手动修改目标行成功。
手动修改任意行后查询无结果,再看错误提示,证明确实是加锁了。筛选下相邻行不能修改,说明确实是在索引加锁了,且条件较强。
重新调试,单步修改后,修改任意行对应列数据,提交成功,重新筛选,目标数据无变化,
仅指定其中一个筛选条件(目标行用三个条件定位),任意数据无法修改,
切换筛选条件,发现可以修改,查看表定义,该列无独立索引,
切换筛选条件,无法修改,查看表定义,该列有独立索引。
综上,调试过程中,和目标行相关的所有符合条件的索引,即便是仅满足其中一个条件,也会对该行加锁。
解决方案
其实算不上解决,调试过程中,注意三点:
- 注意断点前后及调试过程中执行的修改语句,在修改前可手动操作,也可查看到实际数据。
- 断点运行到修改语句后,或用计算器修改了数据后,不要查看数据库数据,这已经不是运行过程中的实际数据了。
- 手动点击继续运行按钮或本轮调试完成后,才可以查看到实际运行后的数据结果,方可进行后续操作(修改、后续处理过程)。
声明:本文使用八爪鱼rpa工具从gitee自动搬运本人原创(或摘录,会备注出处)博客,如版式错乱请评论私信,如情况紧急或久未回复请致邮 xkm.0jiejie0@qq.com 并备注原委;引用本人笔记的链接正常情况下均可访问,如打不开请查看该链接末尾的笔记标题(右击链接文本,点击 复制链接地址,在文本编辑工具粘贴查看,也可在搜索框粘贴后直接编辑然后搜索),在本人博客手动搜索该标题即可;如遇任何问题,或有更佳方案,欢迎与我沟通!