MySQL死锁及解决方案

1 MySQL锁类型

1. MySQL常用存储引擎的锁机制

MyISAM和MEMORY采用表级锁(table-level locking)

BDB采用页面锁(page-level locking)或表级锁,默认为页面锁

InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁

2. 各种锁特点

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高

页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

3. 各种锁的适用场景

表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用

行级锁则更适合于有大量按索引条件并发更新数据,同时又有并发查询的应用,如一些在线事务处理系统。

2 MySQL死锁产生原因

所谓死锁<DeadLock>:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB。

死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。

那么对应的解决死锁问题的关键就是:让不同的session加锁有次序。

3 MySQL死锁举例分析

案例一:

在MySQL中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。在UPDATE、DELETE操作时,MySQL不仅锁定WHERE条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的next-key locking。

例如,一个表db.tab_test,结构如下:

id:主键;

出现死锁日志如下:

***(1) TRANSACTION: 

原因分析:

当“update tab_test set state=1064,time=now() where state=1061 and time < date_sub(now(), INTERVAL 30 minute)”执行时,MySQL会使用idx_1索引,因此首先锁定相关的索引记录,因为idx_1是非主键索引,为执行该语句,MySQL还会锁定主键索引。

假设“update tab_test set state=1067,time=now () where id in (9921180)”几乎同时执行时,本语句首先锁定主键索引,由于需要更新state的值,所以还需要锁定idx_1的某些索引记录。

这样第一条语句锁定了idx_1的记录,等待主键索引,而第二条语句则锁定了主键索引记录,而等待idx_1的记录,这样死锁就产生了。

解决办法

拆分第一条sql,先查出符合条件的主键值,再按照主键更新记录:

select id from tab_test where state=1061 and time < date_sub(now(), INTERVAL 30 minute); 

至此MySQL死锁问题得以解决!

案例二:

在开发中,经常会做这类的判断需求:根据字段值查询(有索引),如果不存在,则插入;否则更新。

以id为主键为例,目前还没有id=22的行

当对存在的行进行锁的时候(主键),mysql就只有行锁。

当对未存在的行进行锁的时候(即使条件为主键),mysql是会锁住一段范围(有gap锁)

锁住的范围为:

(无穷小或小于表中锁住id的最大值,无穷大或大于表中锁住id的最小值)

如:如果表中目前有已有的id为(11 , 12)

那么就锁住(12,无穷大)

如果表中目前已有的id为(11 , 30)

那么就锁住(11,30)

对于这种死锁的解决办法是:

insert into t3(xx,xx) on duplicate key update xx='XX';

用mysql特有的语法来解决此问题。因为insert语句对于主键来说,插入的行不管有没有存在,都会只有行锁。

案例三:

image

一般的情况,两个session分别通过一个sql持有一把锁,然后互相访问对方加锁的数据产生死锁。

案例四:

image

两个单条的sql语句涉及到的加锁数据相同,但是加锁顺序不同,导致了死锁。

4 MySQL死锁总结

**1. 死锁预防策略 **

InnoDB引擎内部(或者说是所有的数据库内部),有多种锁类型:事务锁(行锁、表锁),Mutex(保护内部的共享变量操作)、RWLock(又称之为Latch,保护内部的页面读取与修改)。

InnoDB每个页面为16K,读取一个页面时,需要对页面加S锁,更新一个页面时,需要对页面加上X锁。任何情况下,操作一个页面,都会对页面加锁,页面锁加上之后,页面内存储的索引记录才不会被并发修改。

因此,为了修改一条记录,InnoDB内部如何处理:根据给定的查询条件,找到对应的记录所在页面;对页面加上X锁(RWLock),然后在页面内寻找满足条件的记录;在持有页面锁的情况下,对满足条件的记录加事务锁(行锁:

根据记录是否满足查询条件,记录是否已经被删除,分别对应于上面提到的3种加锁策略之一);

死锁预防策略:相对于事务锁,页面锁是一个短期持有的锁,而事务锁(行锁、表锁)是长期持有的锁。因此,为了防止页面锁与事务锁之间产生死锁。

InnoDB做了死锁预防的策略:持有事务锁(行锁、表锁),可以等待获取页面锁;但反之,持有页面锁,不能等待持有事务锁。根据死锁预防策略,在持有页面锁,加行锁的时候,如果行锁需要等待。则释放页面锁,然后等待行锁。此时,行锁获取没有任何锁保护,因此加上行锁之后,记录可能已经被并发修改。

因此,此时要重新加回页面锁,重新判断记录的状态,重新在页面锁的保护下,对记录加锁。如果此时记录未被并发修改,那么第二次加锁能够很快完成,因为已经持有了相同模式的锁。

但是,如果记录已经被并发修改,那么,就有可能导致本文前面提到的死锁问题。以上的InnoDB死锁预防处理逻辑,对应的函数,是row0sel.c::row_search_for_mysql()。

感兴趣的朋友,可以跟踪调试下这个函数的处理流程,很复杂,但是集中了InnoDB的精髓。

2. 死锁产生的前提

Delete操作,针对的是唯一索引上的等值查询的删除;(范围下的删除,也会产生死锁,但是死锁的场景,跟本文分析的场景,有所不同)。

少有3个(或以上)的并发删除操作;

并发删除操作,有可能删除到同一条记录,并且保证删除的记录一定存在;

事务的隔离级别设置为Repeatable Read,同时未设置innodb_locks_unsafe_for_binlog参数(此参数默认为FALSE);(Read Committed隔离级别,由于不会加Gap锁,不会有next key,因此也不会产生死锁);

使用的是InnoDB存储引擎;(废话!MyISAM引擎根本就没有行锁)。

推荐阅读更多精彩内容

  • 一、概述 数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种...
    不变甄心阅读 2,535评论 0 3
  • 文章导读: 累兮,累兮,要死兮...... 本文解决问题: 1、表级锁定(读锁、写锁) 2、行级锁定(共享锁、排他...
    凡_小火阅读 343评论 0 1
  • 一、简介 数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种...
    huangxiongbiao阅读 189评论 0 0
  • 一、MySQL优化 MySQL优化从哪些方面入手: (1)存储层(数据) 构建良好的数据结构。可以大大的提升我们S...
    宠辱不惊丶岁月静好阅读 1,785评论 0 7
  • 天空中,有很多的雨,有很多的阴暗。 有时候大雪纷飞,有时候大雨滂沱,这天,是一个什么样的天。那雨跟本就没有来由的下...
    添行阅读 95评论 0 3
  • 前段时间在007班级群里看到这样一篇文章《007,被自嗨毁了的地方》,看完之后那感觉就像:和一群小伙伴玩的正开心呢...
    郭来辉阅读 155评论 1 0
  • 昨天下午儿子从幼儿园放学回到院子后与好朋友点点玩,平时我是不参孩子们中间的,清明节假期点妈从成都回来两给点...
    利利lili阅读 324评论 0 1
  • 2019年1月24日 星期四 晴 今天是期...
    丫丫脚步阅读 77评论 0 2
  • 相群 坐标:成都市新都区 多年财务工作经验,努力工作, 认真生活,因认可唇膏品质, 希望尝试传奇今生微营销事业, ...
    哲平chelsea阅读 86评论 0 0