InnoDB非唯一索引导致死锁

死锁日志

收到警报,数据库死锁回滚交易失败

SHOW ENGINE INNODB STATUS; 获取最近发生的deadlock

配置:innodb_print_all_deadlocks并在error log查看

在这里插入图片描述
(图1)

翻译

  • 行号:“1: len 8; hex 000000000000B75; asc”:B75(16进制) = 2933(10进制)。
  • (1)WAITING FOR THIS LOCK TO BE GRANTED:事务(1)等待获取锁
  • (2)HOLDS THIS LOCK(S):事务(2)持有该锁

过程

  • TRANSACTION(1)通过update语句1获取2934行记录锁,等待2933行记录锁释放;
  • TRANSACTION(2)持有2933行记录锁,等待2934行记录锁释放。
  • MYSQL发现死锁:WE ROLL BACK TRANSACTION(1)。

分析

表T结构:

TableNon_uniqueKey_nameColumn_name
T0PRIMARYid
T1mm_no

简化语句:

图1update语句1(id = 2933):update T set flag = 0 where m_no = 123 and f = 1;
图1update语句2(id = 2934):update T set flag = 0 where m_no = 123 and f = 2;

select * from T where m_no = 123;

idm_nofflag
299312310
299412320

explain update T set flag = 0 where m_no = 123 and f = 1;

possible_keys(计划用到的索引)rows(计划查询的行数)
m_no_index2

即使只查询f=1的记录,仍会查询2行

由于MySql是在索引上行锁,两个事务同时用一个key–m_no_index索引,两个事务都需要同时对m_no=123的两条记录上行锁,当两个记录上锁顺序不一样(事务1锁2933行,事务2锁2934行)就有几率发生死锁


解决方案

  • 固定上锁顺序

    • 先select所有的行,然后按照主键id排序,每个事务都按顺序上锁。
      • 每个事务都先上锁2933行,如果没抢到2933行就阻塞等待,不会去抢2934行
      • 优化点:仅select主键可以在当前索引树直接拿到主键id,减少一次回表
    • 缺点:因为每个事务都增加了查询和排序,增加了性能损耗,
  • 重试机制

    • 死锁发生需要一定的巧合,在非唯一索引导致的死锁问题重试在大多数时候不会有问题
    • 缺点:个别事务会发生失败,影响用户体验
  • 避免长时间持有锁,减少死锁概率

    • 避免长事务
    • 优化业务逻辑,在事务尽量接近结束再上锁,而不是事务刚开始的时候
    • 尽早commit

具体需要根据业务量和死锁发生的概率权衡用哪种方案


InnoDB如何发现死锁

配置:innodb_deadlock_detect(默认开)

事务等待图wait-for-graph(有向图)

在这里插入图片描述

一旦有向图形成了环,表示造成死锁,InnoDB报错死锁并回滚相应事务


References

How to Minimize and Handle Deadlocks:https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks-handling.html

对于非唯一索引间隙锁死锁问题,以下是一些较为有效的解决办法: ### 优化事务隔离级别 将事务隔离级别从可重复读(RR)调整为读已提交(RC)。在可重复读隔离级别下,间隙锁和Next - key Lock会被使用,容易导致死锁;而读已提交隔离级别下,通常只会使用行锁,大大减少了间隙锁产生死锁的可能性。例如在MySQL中可以通过以下语句设置隔离级别: ```sql SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; ``` ### 调整业务逻辑 - **缩小事务范围**:尽量减少事务中包含的操作数量和时间,让事务尽快提交或回滚。比如原本一个事务中包含多个查询和更新操作,可以拆分成多个小事务依次执行。 - **按相同顺序访问资源**:确保多个事务按照相同的顺序访问非唯一索引记录和间隙。例如,在多个事务中,都按照索引值从小到大的顺序进行查询和更新操作,避免因为不同的访问顺序导致死锁。 ### 优化查询语句 - **避免范围查询**:如果可能,尽量使用等值查询替代范围查询。因为间隙锁主要在范围查询时发挥作用,等值查询通常只会锁定满足条件的行,减少间隙锁的使用。例如将 `SELECT * FROM table WHERE column BETWEEN 1 AND 10;` 改为 `SELECT * FROM table WHERE column = 5;` - **使用索引优化**:确保查询语句使用合适的索引,避免全表扫描。全表扫描可能会导致间隙锁锁定大量的间隙,增加死锁的风险。可以通过 `EXPLAIN` 语句分析查询语句的执行计划,检查是否使用了预期的索引。 ### 监控和处理死锁 - **开启死锁日志**:在数据库中开启死锁日志记录,方便后续分析死锁发生的原因和场景。例如在MySQL中,可以通过设置 `innodb_print_all_deadlocks` 参数为 `ON` 来开启详细的死锁日志记录。 ```sql SET GLOBAL innodb_print_all_deadlocks = ON; ``` - **重试机制**:当检测到死锁发生时,应用程序可以捕获死锁异常,并在一定次数内进行重试。例如在Java代码中可以使用以下示例代码实现重试机制: ```java int maxRetries = 3; int retryCount = 0; while (retryCount < maxRetries) { try { // 执行数据库操作 break; } catch (SQLException e) { if (isDeadlockException(e)) { retryCount++; // 可以添加适当的延迟,避免立即重试 Thread.sleep(100); } else { throw e; } } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值