死锁剖析

Posted 小兀哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了死锁剖析相关的知识,希望对你有一定的参考价值。

      最近在项目中出现了很多问题,最为常见的就是死锁问题,今天我们来看看死锁究竟是怎么回事。

一、基础普及

1、定义

      死锁是由于并发进程只能按互斥方式访问临界资源等多种因素引起的,并且是一种与执行时间和速度密切相关的错误现象。
      若在一个进程集合中,每一个进程都在等待一个永远不会发生的事件而形成一个永久的阻塞状态,这种阻塞状态就是死锁。

2、必要条件

      互斥mutual exclusion):系统存在着临界资源;
      占有并等待(hold and wait):已经得到某些资源的进程还可以申请其他新资源;
      不可剥夺(no preemption):已经分配的资源在其宿主没有释放之前不允许被剥夺;
      循环等待(circular waiting):系统中存在多个(大于2个)进程形成的封闭的进程链,链中的每个进程都在等待它的下一个进程所占有的资源;

二、mysql锁机制

1、分类

      根据锁的类型分,可以分为共享锁,排他锁,意向共享锁和意向排他锁。
      根据锁的粒度分,又可以分为行锁,页锁和表锁。行锁又分为record lock 锁住某一行记录 ,gap lock 锁住某一段范围中的记录 ,next key lock 是前两者效果的叠加)。

2、特点

  • 表级锁

      开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。对于mysql而言,事务机制更多是靠底层的存储引擎来实现,因此,所以mysql层面只有表锁。

  • 页级锁定

      页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。

  • 行级锁

      开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。mysql支持事务的innodb存储引擎则实现了行锁。

      record lock 锁:只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将会使用表锁。因为MySQL的行锁是针对索引加的锁, 而不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引建,是会出现锁冲突的。

      gap锁:对于键值在条件范围内但并不存在的记录,叫做间隙。InnoDB会对这个间隙加锁,这种锁机制就是所谓的间隙锁(gap锁),这种锁只在RR隔离级别下有效。 InnoDB使用间隙锁的目的:一是为了防止幻读,二是为了满足其恢复和复制的需要。

      next-key锁:是记录锁加上记录之前gap锁的组合。mysql通过gap锁和next-key锁实现RR隔离级别。

3、操作

      对于更新操作(读不上锁),只有走索引才可能上行锁;否则会对聚簇索引的每一行上写锁,实际等同于对表上写锁。
      若多个物理记录对应同一个索引,若同时访问,也会出现锁冲突;
      当表有多个索引时,不同事务可以用不同的索引锁住不同的行,另外innodb会同时用行锁对数据记录(聚簇索引)加锁。
      MVCC并发控制机制下,任何操作都不会阻塞读操作,读操作也不会阻塞任何操作,只因为读不上锁。

4、锁的互斥与共享矩阵

5、常用锁命令

      (1).select ……lock in share mode 获得共享锁。【对于表上意向共享锁;对于读取的每一个行,上行级共享锁】
      (2).select …… for update 获得排他锁【对于表上意向排他锁;对于读取的每一个行,会上行级排他锁】
      (3).insert into target_tab select * from source_tab where …
      (4).create table new_tab as select … From source_tab where …
(3)和(4)在RR隔离级别下,会对source_tab上锁,防止出现幻读;RC隔离级别下,不上锁。
      (5).FLUSH TABLES WITH READ LOCK全局读锁定,锁定数据库中的所有库中的所有表,mysqldump会用到这个命令。

三、项目中出现的问题

1、delete 后insert造成死锁

      这种情况的死锁原因是mySql的间隙锁造成的。例如

表task_queue
Id           taskId
1              2
3              9
10            20
40            41
开启一个会话: session 1
sql> set autocommit=0;
sql> delete from task_queue where taskId = 20;
sql> insert into task_queue values(20, 20);
在开启一个会话: session 2
sql> set autocommit=0;
sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(30, 25);

      在没有并发,或是极少并发的情况下, 这样会可能会正常执行,在Mysql中, 事务最终都是穿行执行, 但是在高并发的情况下, 执行的顺序就极有可能发生改变, 变成下面这个样子:

sql> delete from task_queue where taskId = 20;
sql> delete from task_queue where taskId = 25;
sql> insert into task_queue values(20, 20);
sql> insert into task_queue values(30, 25);

      当我们通过一个参数去删除一条记录的时候, 如果参数在数据库中存在, 那么这个时候产生的是普通行锁, 锁住这个记录, 然后删除, 然后释放锁。如果这条记录不存在,问题就来了, 数据库会扫描索引,发现这个记录不存在, 这个时候的delete语句获取到的就是一个间隙锁,然后数据库会向左扫描扫到第一个比给定参数小的值, 向右扫描扫描到第一个比给定参数大的值, 然后以此为界,构建一个区间, 锁住整个区间内的数据, 一个特别容易出现死锁的间隙锁诞生了。
      这个时候最后一条语句:insert into task_queue values(30, 25); 执行时就会爆出死锁错误。因为删除taskId = 25这条记录的时候,20 – 41 都被锁住了, 他们都取得了这一个数据段的共享锁, 所以在获取这个数据段的排它锁时出现死锁。

      解决办法:前面说了, 通过修改innodb_locaks_unsafe_for_binlog参数来取消间隙锁从而达到避免这种情况的死锁的方式尚待商量, 那就只有修改代码逻辑, 存在才删除,尽量不去删除不存在的记录。

2、insert造成死锁

      这种情况也是由间隙所造成的,不过这是因为单独insert语句的时候,会根据唯一索引产生间隙锁,如果在并发的情况下可能会出现死锁。

3、update造成死锁

      在同一个事务中的两个update语句(针对同一张表,条件不同)
这里不再赘述,有兴趣的可以打开以下网址查看
http://blog.csdn.net/starseeker7/article/details/28632773

总结:

      死锁在我们工作中是经常遇到的,所以我们不仅要学会怎么去处理,还要了解其中是为什么出的问题。这叫知其然还要知其所以然。

以上是关于死锁剖析的主要内容,如果未能解决你的问题,请参考以下文章

InnoDB RR隔离级别下INSERT SELECT两种死锁案例剖析

死锁问题分析,间隙锁

MySql 死锁

MySql 死锁

mysql多个事务同时执行死锁(间隙锁)

白话Mysql的锁和事务隔离级别!死锁间隙锁你都知道吗?