Mysql 死锁是怎么产生的,如何解决

1 什么是死锁

        死锁是指两个或多个事务在执行过程中,因争夺锁资源而造成的相互等待的现象,若无外力干涉它们都将无法继续执行。通俗来说,就是两个或多个事务在等待对方释放锁,从而造成僵持不下,使得整个服务陷入停滞状态。

2 Mysql锁的划分

2.1 从锁的粒度来对mysql锁的划分

表级锁(Table-level Lock):开销小,加锁快;不会出现死锁;每次操作锁住整张表,锁定粒度大,发生锁冲突的概率最高, 并发度最低。
行级锁(Row-level Lock):开销大,加锁慢;会出现死锁;每次操作锁住一行数据,锁定粒度最小,发生锁冲突的概率最低, 并发度也最高。
页级锁(Page-level Lock):开销和加锁时间界于表锁和行锁之间;会出现死锁;每次操作锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,并发度一般。

2.2 从锁的类型来对mysql锁的划分

共享锁-读锁(S锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。事务A对记录添加了S锁,可以对记录进行读操作,不能做修改,其他事务可以对该记录追加S锁,但是不能追加X锁,要追加X锁,需要等记录的S锁全部释放。
排他所-写锁(X锁):当前写操作没有完成前,它会阻断其他写锁和读锁。事务A对记录添加了X锁,可以对记录进行读和修改操作,其他事务不能对记录做读和修改操作。

2.3 从锁的性能来对mysql锁的划分

乐观锁-无锁:一般的实现方式是对记录数据版本进行比对,在数据更新提交的时候才会进行冲突检测,如果发现冲突了,则提示错误信息。
悲观锁-有锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定,再修改的控制方式。共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁范畴。

2.4 InnoDB存储引擎三种行锁模式

记录锁(Record Locks) :仅仅锁住索引记录的一行,在单条索引记录上加锁。(RecordLock锁 是记录锁,RC、RR隔离级别都支持)
record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。
所以说当一条sql没有走任何索引时,那么将会在每一条聚合索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。
间隙锁(Gap Locks) :区间锁, 仅仅锁住一个索引区间(开区间,不包括双端端点)。(GapLock是范围锁,RR隔离级别支持。RC隔离级别不支持)
在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。
间隙锁可用于防止幻读,保证索引间的不会被插入数据。比如在 100、10000中,间隙锁的可能值有 (∞, 100),(100, 10000),(10000, ∞),
临键锁(Next-Key Locks) :记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范围锁,RR隔离级别支持。RC隔离级别不支持)
默认情况下,innodb使用next-key locks来锁定记录。select … for update
但当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
Next-Key Lock在不同的场景中会退化:


比如在 100、10000中,临键锁(Next-Key Locks)的可能有 (∞, 100],(100, 10000] ,这里的关键是左开右闭。

3 事务隔离级别和锁的关系

3.1 数据库事务的隔离级别

数据库事务的隔离级别一共有 4 种,由低到高分别为:

读未提交(READ UNCOMMITTED):所有事务都可以看到其他事务未提交的修改。一般很少使用;
读已提交(READ COMMITTED):Oracle默认隔离级别,事务之间只能看到彼此已提交的变更修改;
可重复读(REPEATABLE READ):MySQL默认隔离级别,同一事务中的多次查询会看到相同的数据行;可以解决不可重复读,但可能出现幻读;
可串行化(SERIALIZABLE):最高的隔离级别,事务串行的执行,前一个事务执行完,后面的事务会执行。读取每条数据都会加锁,会导致大量的超时和锁争用问题;

一般而言,数据库的读已提交(READ COMMITTED)能够满足业务绝大部分场景了。

3.2 事务隔离级别和锁的关系

事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使用的封装,隐藏了底层细节。
锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,对相应操作加不同的锁,就可以防止其他事务同时对数据进行读写操作。
对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在开发中手动的设置锁。
MySQL 默认隔离级别:可重复读, 一般建议改为 RC 读已提交
Oracle、SQLServer默认隔离级别:读已提交

4 死锁产生原因和解决方案

4.1 查看Innodb行锁争用情况

通过 show status like 'innodb_row_lock_%'; 命令可以查询MySQL整体的锁状态,如下:

Innodb_row_lock_current_waits:当前正在阻塞等待锁的事务数量。
Innodb_row_lock_time:MySQL启动到现在,所有事务总共阻塞等待的总时长。
Innodb_row_lock_time_avg:平均每次事务阻塞等待锁时,其平均阻塞时长。
Innodb_row_lock_time_max:MySQL启动至今,最长的一次阻塞时间。
Innodb_row_lock_waits:MySQL启动到现在,所有事务总共阻塞等待的总次数。

4.2 详细介绍死锁的概念

什么是死锁DeadLock?
是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
一个的形象举例:
假设有两个人 A 和 B,他们同时想要通过一扇门进入一个房间,但这扇门只能由一人单独打开。
现在,尴尬了 :
A 想要进入房间1,但门被 B 挡住了,所以 A 无法进入,于是 A 抓住了 B 的右手,不让 B 打开房间2的门。
B 想要进入房间2,但门被 A 挡住了,所以 B 无法进入,于是 B 抓住了 A 的左手,不让 A 打开房间1的门。
现在的情况是:
A 等待着 B 放开 B 的左手,以便自己能打开门进入房间,
B 等待着 A 放开A的右手,以便自己能够进入房间。
这就形成了死锁,因为两个人都在对方放开资源,而对方又不愿意放开自己所持有的资源,导致了相互等待,最终无法继续执行下去。

4.3 表级锁死锁

产生原因:
用户A先访问表1(锁住了表1),然后再访问表2;
用户B先访问表2(锁住了表2),然后再企图访问表1;

用户A和用户B加锁的顺序如下:
用户A--》表1(表锁)--》表2(表锁)
用户B--》表2(表锁)--》表1(表锁)

这时,由于用户B已经锁住表2,用户A必须等待用户B释放表2才能继续,
同样用户B要等用户A释放表1才能继续,这就死锁就产生了。 

解决方案:
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。
仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理。

尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理,必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。

4.4 行级锁死锁

产生原因1:
如果在事务中执行了一条没有索引条件的查询,引发全表扫描,行锁膨胀为表锁(或者等价于
表级锁),
多个这样的锁表事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或
死锁。

解决方案1:
SQL语句中不要使用太复杂的关联多表的查询;
使用explain“执行计划"对SQL语句进行分析,对于有全表扫描和全表锁定的SQL语句,建立相应的索引进行优化。

产生原因2:
两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁。如下图

发生死锁:

查看死锁日志:

show engine innodb status;

产生原因3:每个事务只有一个SQL,但是有些情况还是会发生死锁。
假设有下面的一个表 tt

create table tt(
    id int(32) not null,
    name varchar(50) not null,
    reg_time int(32) not null,
    city varchar(50) ,
    primary key (`name`),
    index index_name(`name`),
    index index_reg_time(`reg_time`)
);

事务1, 假设有下面的一个 session1 会话

update tt set city = "香港"  where name="aaa"

首先,session1 从name 非聚族索引索引出发 , 读到的 [aaa, 1], [aaa, 4] , 会加name索引上的记录[aaa, 1], [aaa, 4] 两个 记录的X锁,
然后,session1 会加聚簇索引上的记录X锁, 聚簇索引上 加锁顺序为先[1] 记录, 后[4] 记录
事务2 假设有下面的一个 session2 会话

Select * from tt  where reg_time>=1000 for update

session2 从reg_time 非聚族索引索引出发 , 读到的 [1000, 4], [1100, 3] , [1200,1], [1300, 2] ,
首先,session2 会加reg_time索引上的记录 [1000, 4], [1100, 3] , [1200,1], [1300, 2] 四个 记录的X锁,
然后,session2 而且会加聚簇索引上的记录X锁, 聚簇索引上 加锁顺序为 [4] 、[3] 、[2] 、[1] ,其中 先[4] 记录, 后[1] 记录
session2 加聚簇索引上的记录X锁时,发现跟session1的加锁顺序正好相反,
两个Session恰好都持有了第一把锁,请求加第二 把锁,死锁就发生了。

解决方案: 如上面的原因2和原因3, 对索引加锁顺序的不一致很可能会导致死锁,所以如果可以,
尽量以相同的顺序来访问索引记录和表。
在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能;通过统一的锁定顺序,可以有效地避免不同事务之间的锁定顺序不一致导致的死锁问题。

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值