MySQL 锁和死锁

#

行锁, 锁优化 [3] #

  • 在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。 知道了这个设定,对我们使用事务有什么帮助呢?那就是,如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放.[todo 加个例子]

  • 行锁是通过索引实现的,如果不通过索引条件检索数据,那么 InnoDB 将对表中所有的记录进行加锁。

  • 行锁的具体实现算法有三种:record lock、gap lock 以及 next-key lock。

    • record lock是专门对索引项加锁;
    • gap lock 是对索引项之间的间隙加锁;
    • next-key lock 则是前面两种的组合,对索引项以其之间的间隙加锁。 只在可重复读或以上隔离级别下的特定操作才会取得 gap lock 或 next-key lock,在Select 、Update 和 Delete 时,除了基于唯一索引的查询之外,其他索引查询时都会获取gap lock 或 next-key lock,即锁住其扫描的范围。

隐式锁和显示锁 #

显示锁 SELECT … LOCK IN SHARE MODE(加共享锁); SELECT … FOR UPDATE(加排他锁);

死锁 #

死锁和死锁检测 [5] #

当出现死锁以后,有两种策略:

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout来设置。 innodb_lock_wait_timeout的默认值是50s。 实际中不用这种策略。

  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事 务得以继续执行。将参数 innodb_deadlock_detect 设置为on,表示开启这个逻辑。

    • 带来的问题:每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是O(n)的操作。假设有1000个并发线程要同时更新同一行,那么死锁检测操作就是100万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的CPU资源

    • 一种解决思路是控制并发度:并发控制要做在数据库服务端。如果有中间件,可以考虑在中间件实现;如果-团队有能修改MySQL源码的人,也可以做在MySQL里面。基本思路就是,对于相同行的更新,-在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。

    • 另一种解决思路是在应用层上优化:你可以考虑通过将一行改成逻辑上的多行来减少锁冲突。 比如,一个账户1条记录变10条记录。

预防死锁 [7] #

  • 减少长事务
  • 大事务拆成小事务
  • 保证加锁顺序一直
  • 业务允许的情况下,降低隔离级别 RR几倍下会有间隙锁,会提高死锁发生的概率

死锁的排查和解决 [7] #

  • 通过日志系统及时通知死锁事件 通过ELK做通知
  • 结合业务代码与死锁日志 进行分析
    • 通过 pt-deadlock-logger 监控死锁
    • 查看最近一次的死锁日志
      show engine innodb status

案例 #

Case [1] #

case

原因 #

死锁是在并发环境下,两个或多个事务互相等待对方持有的资源而无法继续执行的情况。在上文中,死锁的产生是因为两个事务A和事务B都持有间隙(4,+∞)的gap锁,并且两个事务都在等待对方释放锁,导致循环等待而造成死锁。

解决方案 #

  • innodb_lock_wait_timeout 超时时间 - 通用 避免死锁最直观的方法就是在两个事务相互等待时,**当一个事务的等待时间超过设置的某一 阈值,就对这个事务进行回滚,另一个事务就可以继续执行了。**这种方法简单有效,在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的。

  • 替换 幂等性校验 - 非通用 我们还可以使用其它的方式来代替数据库实现幂等性校验。例如,使用 Redis 以及 ZooKeeper 来实现,运行效率比数据库更佳。

参考 #

    1. 《35 | 记一次线上SQL死锁事故:如何避免死锁?》 刘超
    1. 《33 | MySQL调优之事务:高并发场景下的数据库事务调优》 刘超
    1. 《07 | 行锁功过:怎么减少行锁对性能的影响?》 MySQL实战45讲 丁奇
    1. MYSQL死锁的检测与预防