什么是网站国内高速空间,扬州将建设网站,电子商务专业网站,景区网站建设策划最近我们在排查一个诡异的 空指针异常#xff0c;整个分析过程可以说是跌宕起伏#xff0c;最终的结论也颇具隐蔽性。今天就把这个问题分享出来#xff0c;希望对大家有所帮助。
问题现象
在系统中#xff0c;我们有 单据 B#xff0c;它通过一个 关联 ID 字段与 上级单…最近我们在排查一个诡异的 空指针异常整个分析过程可以说是跌宕起伏最终的结论也颇具隐蔽性。今天就把这个问题分享出来希望对大家有所帮助。
问题现象
在系统中我们有 单据 B它通过一个 关联 ID 字段与 上级单据 A 关联。
但在某次操作中我们发现
单据 B 存在并且存储了 A 的 ID。但查询 A 时却查不到数据导致后续代码调用 A 的 get 方法时报空指针异常。
理论上B 既然存储了 A 的 IDA 就应该存在否则 A 的 ID 是怎么来的
难道 A 被删除了 然而代码中并没有删除 A 的逻辑而且 DBA 查询了数据库日志确认 A 从未被删除。那么这就只剩下一种可能A 从未生成。
代码分析
我们回溯代码A 和 B 是在同一个事务内生成的具体逻辑如下
Transactional
public void createA() {DB生成单据A;执行业务方法C;DB生成单据B;
}代码逻辑很简单
第一步 生成 A。第二步 执行 业务方法 C。第三步 生成 B并存储 A 的 ID。
由于 B 存储了 A 的 ID说明 DB生成单据A 代码应该成功执行了。但为什么 A 最终没有出现在数据库中
难道是 createA() 过程中发生了异常导致 A 没有生成 我们查询当时的日志发现 业务方法 C 在执行时发生了 死锁异常 业务方法 C 的代码如下
Transactional
public void doC() {try {DB生成C;} catch (Exception e) {输出异常日志;}
}
在 DB生成C 这一步时数据库发生了死锁异常但代码使用了 try-catch所以理论上不会影响事务的执行A 和 C 都应该正常生成。
那么问题来了A 为什么消失了
问题的根本原因MySQL 隐式回滚
最终DBA 通过查询数据库的日志发现了问题的真正原因——MySQL 发生了“隐式回滚”。 MySQL 死锁处理机制 在 MySQL 中当多个事务发生死锁时数据库会自动选择一个代价较低的事务进行回滚以解除死锁。这一行为是 数据库层面的自动回滚不会受到 try-catch 代码的影响。
在本例中事务执行时发生了死锁MySQL 自动回滚了整个事务 createA()导致 A 被回滚实际上根本没被写入数据库。
但由于 doC() 代码中使用了 try-catch异常并没有往上抛导致事务继续执行到了 DB生成单据B;。由于 B 在一个新的事务中生成它最终成功入库并存储了 已被回滚的 A 的 ID从而导致数据不一致的问题。
完整过程如下
DB生成单据A; → 执行成功暂时DB生成C; → 发生死锁MySQL 选择回滚事务 createA()A 被回滚由于 try-catch 捕获了异常事务继续执行DB生成单据B; → B 在新的事务中成功插入并存储了已回滚的 A 的 ID
最终导致 B 关联了一个不存在的 A后续调用 A 的 get 方法时报空指针异常。
如何避免类似问题
通过这次分析我们可以总结出几点避免类似问题的经验
避免在事务中吞掉异常 try-catch 不能仅仅记录日志如果异常影响了事务的完整性应该显式回滚整个事务。 改进 doC()方法 Transactional
public void doC() {try {DB生成C;} catch (Exception e) {log.error(生成 C 失败, e);throw e; // 让事务感知异常避免错误继续执行}
}
尽量控制事务粒度避免长时间持有锁 业务方法 C 的执行时间过长可能加剧死锁风险。 可以考虑将 业务方法 C 放到事务外部执行避免影响 A 和 B 的创建 Transactional
public void createA() {DB生成单据A;DB生成单据B;
}public void doC() {DB生成C; // 独立事务避免影响 A、B
} 调整执行顺序将c方法挪至最后 Transactional
public void createA() {DB生成单据A;DB生成单据B;执行业务方法C;
}
因为B方法的trycatch逻辑因为业务原因没法改所以我这边采用了3的方法并且同时优化了B方法降低了死锁发生的概率。希望这次排查经历能给大家一些启发