在事务和重试机制下,死锁导致的数据丢失。

一、问题背景

1.工单更新接口调用成功,但是工单的主表字段没有变更。

2.工单的表结构分为主表和扩展表。查看数据库,扩展表更新成功了,但是主表没有更新。

3.主表和扩展表的更新都在一个事务下面,扩展表的更新操作有重试机制。

更新工单代码示例:

扩展表创建语句:

二、问题排查

由于扩展表更新了,但是主表没更新,主表更新的语句在扩展表之前,初步判断是回滚导致的,数据丢失。

1.是否是异常导致的回滚

查询日志发现主表和扩展表都有更新日志,按道理来说两张表都应该更新完成了。继续查看日志,扩展表在更新时有抛出死锁的日志,查看死锁的栈信息,重试机制吃掉了抛出的死锁异常。最后在重试下更新扩展表成功了,因此此处的回滚不可能是事务捕获到异常进行的回滚。

2.是否是mybatis导致的回滚

mybatis在执行异常时,只会释放引用,并不会处理事务,因此不是mybatis导致的回滚,只可能是InnoDB里面发生了回滚。

3.DB发生了回滚

查阅MySQL官方文档https://dev.mysql.com/doc/refman/8.4/en/innodb-deadlock-detection.html发现在发生死锁时,数据库内核会自动回滚较小的事务,来避免死锁的持续。(该处的较小事务的大小由插入更新或者删除决定)

三、解决方案

方案1:使用REQUIRES_NEW,创建新事务后,在此处较小的事务的大小就等于新事务了。

优势:改造简单,成本低

劣势:没从根本上解决死锁问题。

方案2:先更新扩展表,再更新主表。

优势:不引入新事务。

劣势:可能影响其他逻辑。

方案3: 删除区分度低的索引,避免死锁。

优势:从根本上解决死锁

劣势:在没有该索引后,往常从该索引查询的语句效率会变低。因此依靠被删索引的查询语句查扩展表需要走ES,并且需要考虑写库后立即查的情况(es存在主从延迟的问题)。

考虑时间成本和改动范围,最终选择方案一。代码如下:

四、结论

1.死锁的原因:是因为扩展表的“idx_field_id_field_value25”索引区分度低(存在过多与索引相关的相同字段值)。

2.回滚的原因:发现在发生死锁时,数据库内核会自动回滚较小的事务,来避免死锁的持续。

3.最开始使用Propagation.NESTED事务的原因:想创建一个安全点,使得回滚只影响到updateExpansionTable方法,但是Propagation.NESTED在内层只会创建安全点,不会创建新事务,而死锁的回滚会跳过安全点直接回滚事务,导致没达到预期情况。

五、补充

1 死锁

死锁通常发生在多个事务同时访问相同的资源,并且每个事务都在等待另一个事务释放资源的情况下。不存在事务的情况下,不会发生死锁,因为资源的锁定和释放是即时的,不会出现等待的情况,因此不会发生死锁。

一个工单的更新通常包含:一条主表记录的更新和多条扩展表记录的更新(查看扩展表的create语句就能发现一条扩展表记录是对应工单的一个字段,而工单通常不止更新一个字段)。在实际的代码中,就对更新语句涉及的扩展表记录进行了排序,保证每个工单进行更新时,工单的扩展字段的都是有序的,以此来降低死锁发生的可能性。

2.索引区分度

2.1 简介:

索引的区分度低是指索引列中的重复值较多,导致索引不能有效区分不同的记录。区分度低的索引在查询时会导致大量的记录被扫描,从而降低查询效率,并可能引发锁争用和死锁问题。

在一个包含大量记录的表中,如果某个列的值大部分都是相同的,那么以该列为基础创建的索引的区分度就很低。查询时,数据库需要扫描大量的记录才能找到匹配的结果,这不仅影响性能,还可能导致多个事务同时竞争相同的锁,从而引发死锁。例如,此处的idx_field_id_field_value25 的fieldId 为110代表结费方式,field_value的值为1代表上门、为2代表远程,如果每个工单都存在节费方式的属性,那么这个 idx_field_id_field_value25 的索引就极其容易重复。

2.2 索引的区分度低可能会导致数据库中的死锁问题。以下是一些常见的解决方法:

1.优化索引:确保索引的选择性高,避免使用低区分度的列作为索引。

2.减少事务持有锁的时间:尽量缩短事务的执行时间,减少锁的持有时间。

3.合理设计事务顺序:确保多个事务访问资源的顺序一致,避免循环等待。

4.使用合适的隔离级别:根据业务需求选择合适的事务隔离级别,避免不必要的锁争用。

5.分解大事务:将大事务分解为多个小事务,减少锁的竞争。

2.3可以通过以下SQL语句查看索引的区分度:

SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivity

FROM table_name;

3事务传播性

3.1事务介绍

3.2此处两事务对比

Propagation.NESTED 和 Propagation.REQUIRES_NEW 是Spring事务管理中的两种传播行为,它们的区别如下:

* Propagation.NESTED:

   - 嵌套事务。

   - 如果当前存在一个事务,则在嵌套事务中执行。如果当前没有事务,则创建一个新的事务。

   - 嵌套事务依赖于外部事务,嵌套事务的提交或回滚不会影响外部事务,但外部事务的回滚会导致嵌套事务也回滚。

* Propagation.REQUIRES_NEW:

   - 新建事务。

   - 无论当前是否存在事务,都创建一个新的事务。当前事务会被挂起,直到新事务完成。

   - 新事务的提交或回滚不会影响挂起的事务,两个事务相互独立。

总结:

- Propagation.NESTED 适用于需要嵌套事务的场景,嵌套事务依赖于外部事务。

- Propagation.REQUIRES_NEW 适用于需要完全独立事务的场景,新事务与外部事务相互独立。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值