MySQL -- 锁,阻塞与死锁

事务的隔离级别

隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。

脏读: 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做”脏读”.

不可重复读: 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。

幻读: 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本上可分为以下两种。

1.一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。
2.另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。

MySQL 的四种隔离级别(InnoDB 默认 可重复读(Repeated Read))

未提交读(Read Uncommitted):
允许脏读,也就是可能读取到其他会话中未提交事务修改的数据

提交读(Read Committed): 只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)

可重复读(Repeated Read): 可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB(mysql)默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻读

串行读(Serializable): 完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

InnoDB行锁实现方式

InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

1、 在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁。

2、 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
如表 tab_with_index 对 id 加索引,有下面记录
+——+——+
| id | name |
+——+——+
| 1 | 1 |
| 1 | 4 |
| 2 | 2 |
+——+——+

对于 session_1:

1
2
3
4
// 设置不自动提交
mysql> set autocommit=0;
mysql> select * from tab_with_index where id = 1 and name = '1' for update;
// 此时未提交

对于 session_2 :

1
2
3
// 虽然 session_2 访问的是和 session_1 不同的记录,但是因为使用了相同的索引,所以需要等待锁:
mysql> select * from tab_with_index where id = 1 and name = '4' for update;
//已提交,但是等待 session_1 释放锁

3、 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
如,同样是上面的例子,再对 name 加索引,

对于 session_1:

1
2
3
4
// 设置不自动提交
mysql> set autocommit=0;
mysql> select * from tab_with_index where id = 1 for update;
// 此时未提交

对于 session_2 :

1
2
3
4
5
6
7
// Session_2使用name的索引访问记录,因为记录没有被索引,所以可以获得锁:
mysql> select * from tab_with_index where name = '2' for update;
1 row in set (0.00 sec)

// 由于访问的记录已经被session_1锁定,所以等待获得锁。:
mysql> select * from tab_with_index where name = '4' for update;
//已提交,但是等待 session_1 释放锁

4、 即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,
如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

数据库阻塞

阻塞: 第一个连接占有资源没有释放,而第二个连接需要获取这个资源。如果第一个连接没有提交或者回滚,第二个连接会一直等待下去,直到第一个连接释放该资源为止。上面出现的等待现象就是阻塞,当session_1提交,阻塞就消失。

数据库死锁

死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。表级锁不会产生死锁.所以解决死锁主要还是针对于最常用的InnoDB。
死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。
那么对应的解决死锁问题的关键就是:让不同的session加锁有次序。

如何避免死锁:
1、 为了减少死锁的可能性,请使用事务而不是LOCK TABLES语句;
2、 保持插入或更新数据的事务足够小,以使其长时间不保持打开状态;因为大事物占用资源多耗时长,与其他事物冲突的概率也会变大。
3、 当不同的事务更新多个表或大范围的行时,请SELECT … FOR UPDATE在每个事务中使用相同的操作顺序。
4、 在SELECT … FOR UPDATE和 UPDATE … WHERE 语句中使用的列上创建索引。如果不走索引会为表的每一行记录加锁,死锁的概率会大大增加。

解除死锁的两种方法:
1、 终止(或撤销)进程: 终止(或撤销)系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态中解除出来。
2、 抢占资源: 从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以打破死锁状态。