一、背景知识
Mysql innodb如何进行数据读取,什么样的数据读取需要加锁,数据隔离级别是什么样的,什么情况下该使用什么类型的锁,锁定的方式又是什么,在本小节梳理了相关背景知识,解答了以上疑问,以更清晰地了解锁机制及死锁产生的原因。
1.1.MVCC:快照读(Snapshot Read)与当前读(Current Read)
MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。其最大的优点是:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。
MVCC读取数据的方式分为快照读和当前读两种方式。快照读:读取指定版本,不需要加锁,如简单的select操作;当前读:读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。如插入、更新、删除操作,均需要进行加锁。之所以将这些操作归为当前读,用更新操作举例,在进行更新操作前,首先会读取当前的待更新数据库字段的值进行返回并加锁(当前读),待mysql收到这个加锁记录后,才会发起一个update的操作,更新该记录。删除操作同理,插入操作稍有不同,简单地说在执行插入前可能会触发唯一键检查操作,也会进行一个当前读。
1.2.事务的四种隔离级别
在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。数据库锁也是为了构建这些隔离级别存在的。
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
- 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
- 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
- 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
- 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
1.3.数据库锁类型
根据对资源操作的不同,对资源锁定的级别也有所不同。MySQL InnoDB存储引擎的锁类型分为以下几种。
锁类型 | 描述 |
---|---|
共享锁(S) | 允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。 |
排他锁(X) | 允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。 |
意向共享锁(IS) | 事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。 |
意向排他锁(IX) | 事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。 |
上述锁类型之间的兼容关系如下
X | IX | S | IS | |
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。对于当前读操作,InnoDB会自动给涉及数据集加排他锁(X);对于快照读操作,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
用SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT... FOR UPDATE方式获得排他锁。
1.4.锁定方式
mysql innodb支持三种行锁定方式:
行锁(Record Lock):锁直接加在索引记录上面,锁住的是key。
间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别。
Next-Key Lock :行锁和间隙锁组合起来就叫Next-Key Lock。
默认情况下,InnoDB工作在可重复读隔离级别下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。
二、数据库死锁定义及其产生的必要条件
所谓数据库死锁是指两个或多个事务,各自占有对方的期望获得的资源,形成的循环等待,彼此无法继续正常执行的一种状态。
从业务层看死锁产生具有一定的概率性,当具备了以下几个必要条件时,则会出现死锁:
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
三、死锁问题示例及解决方法
示例1:
t_example1表 (F_column_1 primary key, F_id)
F_id | F_column_1 | …… |
---|---|---|
1 | 1 | |
2 | 2 | |
3 | 3 |
t_example2表(F_column_2 primary key, F_id)
F_id | F_column_2 | …… |
---|---|---|
1 | 1 | |
2 | 2 | |
3 | 3 |
session 1 | session 2 |
---|---|
begin transaction | begin transaction |
select * from t_example1 where F_column_1=1 for update | |
select * from t_example2 where F_column_2=3 for update | |
select * from t_example2 where F_column_2=3 for update | |
select * from t_example1 where F_column_1=1 for update |
分析:上述死锁示例是比较常见的一种死锁,由于两个事务分别持有了一个行锁,分别等待对方释放所持有的行锁,因此导致了死锁。
示例2:
t_example3(F_id primary key, F_user_id )建立的索引为key (F_user_id)
F_id | F_user_id | …… |
---|---|---|
1 | 1 | |
2 | 3 | |
3 | 5 |
session 1 | session 2 |
---|---|
begin transaction | begin transaction |
select XX from t_example3 where F_user_id=3 for update | |
update t_example1 set XX where F_column_1=1 | |
update t_example1 set XX where F_column_1=1 | |
insert into t_example3 (F_id,F_user_id)values(4,2) |
分析:示例2之所以会产生死锁,因为表t_example3对F_user_id创建的是一个普通索引,session1在根据F_user_id查询条件执行select for update时,锁定的是上一条记录到下一条记录之间的区间,因此,该锁锁定区间为从记录1到5之间的区间,session2事务首先持有了表example1的F_column_1的行锁,接下来想要向表三种插入F_user_id=2的记录,其需要锁定的是记录1到记录3之间的区间,而其所尝试锁定的部分区间已经被session1的事务锁定,因此进入了等待状态,此时,session1的事务等待session2的事务所持有的表t_example1的F_column_1=1的行锁的释放,因此出现了死锁。
当出现死锁后,通常采用破坏死锁产生的四个必要条件其中的一个或多个,来解决死锁问题。
1)确保资源按照指定的顺序推进,破坏环状等待的现状
上述示例一解决方法:将其中一个事务的sql执行顺序调整,确保加锁的顺序一致,从而避免死锁。
2)尽可能缩小锁定资源的区间,避免gap锁所导致的死锁问题
上述示例二解决方法:将t_example3表的索引由普通索引改成唯一索引,缩小锁定区间,解决间隙锁导致的死锁问题。
3)设置锁超时时间,达到指定阈值后自动释放锁,破坏请求并保持条件
这种方式相对被动,通过判断锁持有时间,超过指定阈值后,则释放其所持有的锁定资源,交由业务自身重试处理,使等待该资源的其他事务得以推进。目前的分布式数据库死锁,则是采用的锁超时的处理方式来解决不同分片上出现的死锁问题。